Jak nahrát soubory do Amazon S3 pomocí File Reader API

Jak pomocí FileReader API v prohlížeči načíst soubor do paměti jako řetězec base64 a nahrát jej do Amazon S3 pomocí aws-sdk knihovny z NPM.

Začínáme

Pro tento tutoriál budeme potřebovat back-end a front-end. Náš back-end bude použit ke komunikaci s Amazon S3, zatímco front-end nám poskytne uživatelské rozhraní, kam můžeme nahrát náš soubor.

Abychom to urychlili, použijeme CheatCode's Node.js Boilerplate pro back-end a CheatCode's Next.js Boilerplate pro front-end. Abychom získali tato nastavení, musíme je naklonovat z Github.

Začneme back-endem:

Terminál

git clone https://github.com/cheatcode/nodejs-server-boilerplate.git server

Po klonování cd do projektu a nainstalujte jeho závislosti:

Terminál

cd server && npm install

Dále musíme nainstalovat jednu další závislost, aws-sdk :

Terminál

npm i aws-sdk

Jakmile jsou všechny závislosti nainstalovány, spusťte server pomocí:

Terminál

npm run dev

Když je váš server spuštěný, v jiném okně nebo kartě terminálu musíme naklonovat front-end:

Terminál

git clone https://github.com/cheatcode/nextjs-boilerplate.git client

Po klonování cd do projektu a nainstalujte jeho závislosti:

Terminál

cd client && npm install

Jakmile jsou všechny závislosti nainstalovány, spusťte front-end pomocí:

Terminál

npm run dev

S tím jsme připraveni začít.

Zvýšení limitu analyzátoru těla

Když se podíváme na kód našeho serveru, první věc, kterou musíme udělat, je upravit limit nahrávání pro body-parser middleware ve standardu. Tento middleware je zodpovědný, jak název napovídá, za analýzu nezpracovaných dat těla požadavku HTTP odeslaného na server (server Express.js).

/server/middleware/bodyParser.js

import bodyParser from "body-parser";

export default (req, res, next) => {
  const contentType = req.headers["content-type"];

  if (contentType && contentType === "application/x-www-form-urlencoded") {
    return bodyParser.urlencoded({ extended: true })(req, res, next);
  }

  return bodyParser.json({ limit: "50mb" })(req, res, next);
};

V Express.js je middleware termín používaný k označení kódu, který běží mezi požadavkem HTTP, který původně zasáhl server, a předáním odpovídající cestě/cestě (pokud existuje).

Výše uvedená funkce, kterou exportujeme, je funkce middlewaru Express.js, která je součástí CheatCode Node.js Boilerplate. Tato funkce přijímá požadavek HTTP z Express.js – můžeme identifikovat, že se jedná o požadavek, který nám Express předává pomocí req , res a next argumenty, které Express předává zpětným voláním směrování – a poté předá tento požadavek příslušné metodě z body-parser závislost zahrnuta v základním popisu.

Myšlenka je taková, že chceme použít vhodný "konvertor" z bodyParser abychom zajistili, že nezpracovaná data těla, která získáme z požadavku HTTP, budou použitelná v naší aplikaci.

Pro tento tutoriál budeme z prohlížeče odesílat data ve formátu JSON. Můžeme tedy očekávat, že všechny požadavky, které odešleme (nahrání souborů), budou předány bodyParser.json() metoda. Výše vidíme, že předáváme objekt s jednou vlastností limit nastavte na 50mb . To se dostane kolem výchozího limit z 100kb na těle požadavku HTTP uloženém knihovnou.

Protože nahráváme soubory různé velikosti, musíme tuto velikost zvýšit, abychom při nahrávání neobdrželi žádné chyby. Zde používáme "nejlepší odhad" 50 megabajtů jako maximální velikost těla, kterou obdržíme.

Přidání cesty Express.js

Dále musíme přidat trasu, kam budeme posílat naše uploady. Jak jsme naznačili výše, používáme Express.js ve standardu. Abychom udrželi náš kód uspořádaný, rozdělili jsme různé skupiny tras, ke kterým se přistupuje pomocí funkcí volaných z hlavního index.js soubor, kde je expresní server spuštěn v /server/index.js .

Tam zavoláme funkci api() který načte trasy související s API pro standardní verzi.

/server/api/index.js

import graphql from "./graphql/server";
import s3 from "./s3";

export default (app) => {
  graphql(app);
  s3(app);
};

V tomto souboru pod voláním graphql() , chceme přidat další volání funkce s3() které vytvoříme příště. Zde app představuje instanci aplikace Express.js, do které přidáme naše trasy. Vytvořme to s3() fungovat nyní.

/server/api/s3/index.js

import uploadToS3 from "./uploadToS3";

export default (app) => {
  app.use("/uploads/s3", async (req, res) => {
    await uploadToS3({
      bucket: "cheatcode-tutorials",
      acl: "public-read",
      key: req.body?.key,
      data: req.body?.data,
      contentType: req.body?.contentType,
    });

    res.send("Uploaded to S3!");
  });
};

Zde vezmeme expresní app instanci, kterou jsme předali a zavolali .use() metodou, předávání cesty, kde chceme, aby byla naše trasa dostupná, /uploads/s3 . Uvnitř zpětného volání pro trasu voláme funkci uploadToS3 který definujeme v další části.

Je důležité si uvědomit:máme v úmyslu uploadToS3 vrátit příslib JavaScriptu. To je důvod, proč máme await klíčové slovo před metodou. Když provádíme nahrávání, chceme „čekat na“ vyřešení slibu, než odpovíme na původní požadavek HTTP, který jsme odeslali od klienta. Aby to také fungovalo, přidali jsme předponu klíčového slova async na funkci zpětného volání naší trasy. Bez toho JavaScript vyhodí chybu o await jako vyhrazené klíčové slovo při spuštění tohoto kódu.

Pojďme na to uploadToS3 a podívejte se, jak předat naše soubory AWS.

Zapojení nahrávání do Amazon S3 na serveru

Nyní k tomu důležitému. Abychom přenesli naše nahrávání na Amazon S3, musíme nastavit připojení k AWS a instanci .S3() metoda v aws-sdk knihovny, kterou jsme dříve nainstalovali.

/server/api/s3/uploadToS3.js

import AWS from "aws-sdk";
import settings from "../../lib/settings";

AWS.config = new AWS.Config({
  accessKeyId: settings?.aws?.akid,
  secretAccessKey: settings?.aws?.sak,
  region: "us-east-1",
});

const s3 = new AWS.S3();

export default async (options = {}) => { ... };

Než skočíme do těla naší funkce, musíme nejprve zapojit instanci AWS. Přesněji řečeno, musíme předat ID přístupového klíče AWS a tajný přístupový klíč. Tato dvojice dělá dvě věci:

  1. Ověří náš požadavek pomocí AWS.
  2. Ověřuje, že tento pár má správná oprávnění pro akci, kterou se snažíme provést (v tomto případě s3.putObject() ).

Získání těchto klíčů je mimo rozsah tohoto kurzu, ale přečtěte si tuto dokumentaci od Amazon Web Services, abyste se naučili, jak je nastavit.

Za předpokladu, že jste získali své klíče – nebo máte existující pár, který můžete použít – dále využijeme implementaci nastavení v CheatCode Node.js Boilerplate k bezpečnému uložení našich klíčů.

/server/settings-development.json

{
  "authentication": {
    "token": "abcdefghijklmnopqrstuvwxyz1234567890"
  },
  "aws": {
    "akid": "Type your Access Key ID here...",
    "sak":" "Type your Secret Access Key here..."
  },
  [...]
}

Uvnitř /server/settings-development.json , výše přidáme nový objekt aws , nastavte jej na stejný jako jiný objekt se dvěma vlastnostmi:

  • akid - Toto bude nastaveno na ID přístupového klíče, které získáte od AWS.
  • sak - Toto bude nastaveno na tajný přístupový klíč, který získáte od AWS.

Uvnitř /server/lib/settings.js , tento soubor se automaticky načte do paměti při spuštění serveru. Všimněte si, že tento soubor se nazývá settings-development.json . -development část nám říká, že tento soubor bude načten pouze při process.env.NODE_ENV (aktuální prostředí Node.js) se rovná development . Podobně v produkci bychom vytvořili samostatný soubor settings-production.json .

Smyslem toho je zabezpečení a vyhýbání se používání produkčních klíčů ve vývojovém prostředí. Samostatné soubory zabraňují zbytečnému úniku a míchání klíčů.

/server/api/s3/uploadToS3.js

import AWS from "aws-sdk";
import settings from "../../lib/settings";

AWS.config = new AWS.Config({
  accessKeyId: settings?.aws?.akid,
  secretAccessKey: settings?.aws?.sak,
  region: "us-east-1",
});

const s3 = new AWS.S3();

export default async (options = {}) => { ... };

Zpět v našem uploadToS3.js Dále importujeme settings soubor, který jsme zmínili výše z /server/lib/settings.js a z toho vezmeme aws.akid a aws.sak hodnoty, které jsme právě nastavili.

Nakonec, než se pustíme do definice funkce, vytvoříme novou instanci S3 třídy a uložte jej do s3 proměnná s new AWS.S3() . S tímto přejdeme k jádru naší funkce:

/server/api/s3/uploadToS3.js

import AWS from "aws-sdk";

[...]

const s3 = new AWS.S3();

export default async (options = {}) => {
  await s3
    .putObject({
      Bucket: options.bucket,
      ACL: options.acl || "public-read",
      Key: options.key,
      Body: Buffer.from(options.data, "base64"),
      ContentType: options.contentType,
    })
    .promise();

  return {
    url: `https://${options.bucket}.s3.amazonaws.com/${options.key}`,
    name: options.key,
    type: options.contentType || "application/",
  };
};

Není toho moc, takže jsme sem všechno odhlásili. Základní funkce, kterou budeme volat na s3 instance je .putObject() . Na .putObject() , předáme objekt options s několika nastaveními:

  • Bucket – Segment Amazon S3, kam chcete uložit objekt (výraz S3 pro soubor), který nahrajete.
  • ACL - "Seznam řízení přístupu", který chcete použít pro oprávnění k souboru. To říká AWS, kdo má povolen přístup k souboru. Zde můžete předat kteroukoli z předpřipravených ACL, kterou Amazon nabízí (používáme public-read udělit otevřený přístup).
  • Key - Název souboru tak, jak bude existovat v bucketu Amazon S3.
  • Body – Obsah souboru, který nahráváte.
  • ContentType – Typ MIME pro soubor, který nahráváte.

Zaměření na Body , můžeme vidět, že se děje něco jedinečného. Zde voláme na Buffer.from() metoda, která je zabudována do Node.js. Jak za chvíli uvidíme, když získáme náš soubor zpět z FileReaderu v prohlížeči, bude naformátován jako řetězec base64.

Aby bylo zajištěno, že AWS dokáže interpretovat data, která mu odešleme, musíme převést řetězec, který jsme předali od klienta, do vyrovnávací paměti. Zde předáme naše options.data —řetězec base64 — jako první argument a poté base64 jako druhý argument nechat Buffer.from() znát kódování, ze kterého je potřeba řetězec převést.

Díky tomu máme to, co potřebujeme, zapojené, abychom mohli odeslat do Amazonu. Aby byl náš kód čitelnější, zřetězujeme .promise() metoda na konec našeho volání s3.putObject() . To říká aws-sdk že chceme, aby vrátil JavaScript Promise.

Stejně jako jsme viděli v našem zpětném volání trasy, musíme přidat async klíčové slovo do naší funkce, abychom mohli použít await klíčové slovo „čekejte na“ odpověď od Amazonu S3. Technicky vzato, nepotřebujeme čekat na odpověď S3 (mohli bychom vynechat async/await zde), ale když tak učiníme v tomto tutoriálu, pomůže nám to ověřit, zda je nahrávání dokončeno (více o tom, až zamíříme ke klientovi).

Jakmile je naše nahrávání dokončeno, z naší funkce vrátíme objekt popisující url , name a type souboru, který jsme právě nahráli. Zde si všimněte, že url je naformátován na adresu URL souboru tak, jak existuje ve vašem bucketu Amazon S3.

Tím jsme se serverem skončili. Pojďme ke klientovi, abychom propojili naše rozhraní pro nahrávání a zprovoznili to.

Zapojení FileReader API na klientovi

Protože na klientovi používáme Next.js, vytvoříme nový upload stránce v našem /pages adresář, který bude hostit ukázkovou komponentu s naším nahrávacím kódem:

/client/pages/upload/index.js

import React, { useState } from "react";
import pong from "../../lib/pong";

const Upload = () => {
  const [uploading, setUploading] = useState(false);

  const handleUpload = (uploadEvent) => { ... };

  return (
    <div>
      <header className="page-header">
        <h4>Upload a File</h4>
      </header>
      <form className="mb-3">
        <label className="form-label">File to Upload</label>
        <input
          disabled={uploading}
          type="file"
          className="form-control"
          onChange={handleUpload}
        />
      </form>
      {uploading && <p>Uploading your file to S3...</p>}
    </div>
  );
};

Upload.propTypes = {};

export default Upload;

Nejprve jsme nastavili komponentu React s tolika značkami, abychom získali základní uživatelské rozhraní. Pro styling se spoléháme na Bootstrap, který se nám automaticky nastaví ve standardu.

Důležitou součástí je zde <input type="file" /> což je vstupní soubor, připojíme FileReader příklad k. Když vybereme soubor pomocí tohoto, onChange bude zavolána funkce, která předá událost DOM obsahující naše vybrané soubory. Zde definujeme novou funkci handleUpload které použijeme pro tuto událost.

/client/pages/upload/index.js

import React, { useState } from "react";
import pong from "../../lib/pong";

const Upload = () => {
  const [uploading, setUploading] = useState(false);

  const handleUpload = (uploadEvent) => {
    uploadEvent.persist();
    setUploading(true);

    const [file] = uploadEvent.target.files;
    const reader = new FileReader();

    reader.onloadend = (onLoadEndEvent) => {
      fetch("http://localhost:5001/uploads/s3", {
        method: "POST",
        mode: "cors",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          key: file.name,
          data: onLoadEndEvent.target.result.split(",")[1],
          contentType: file.type,
        }),
      })
        .then(() => {
          setUploading(false);
          pong.success("File uploaded!");
          uploadEvent.target.value = "";
        })
        .catch((error) => {
          setUploading(false);
          pong.danger(error.message || error.reason || error);
          uploadEvent.target.value = "";
        });
    };

    reader.readAsDataURL(file);
  };

  return (
    <div>
      <header className="page-header">
        <h4>Upload a File</h4>
      </header>
      <form className="mb-3">
        <label className="form-label">File to Upload</label>
        <input
          disabled={uploading}
          type="file"
          className="form-control"
          onChange={handleUpload}
        />
      </form>
      {uploading && <p>Uploading your file to S3...</p>}
    </div>
  );
};

Upload.propTypes = {};

export default Upload;

Vyplňte handleUpload funkce, musíme udělat několik věcí. Nejprve přímo do těla funkce přidáme volání .persist() Reactu metoda na uploadEvent (toto je událost DOM předaná prostřednictvím onChange metodou na našem <input /> ). Musíme to udělat, protože React vytváří něco známého jako syntetická událost, která není dostupné uvnitř funkcí mimo hlavní prováděcí vlákno (více o tom za chvíli).

Následně použijeme useState() háček z Reactu k vytvoření stavové proměnné uploading a přepněte jej na true . Pokud se podíváte dolů do našeho označení, můžete vidět, že to používáme k deaktivaci vkládání souboru, zatímco jsme uprostřed nahrávání, a zobrazení zpětné vazby, která potvrzuje, že proces probíhá.

Poté se vrhneme na základní funkce. Nejprve musíme získat soubor, který jsme vybrali z prohlížeče. K tomu zavoláme uploadEvent.target.files a pomocí JavaScript Array Destructuring „odtrhnout“ první soubor v poli souborů a přiřadit jej k proměnné file .

Dále vytvoříme naši instanci FileReader() v prohlížeči. Toto je integrováno do moderních prohlížečů, takže není co importovat.

Jako odpověď dostáváme zpět reader instance. Přeskakování za reader.onloadend na sekundu na konci našeho handleUpload funkce, máme volání reader.readAsDataURL() , předáním file právě jsme destruovali z uploadEvent.target.files pole. Tento řádek je zodpovědný za to, že čtenáři souboru sdělí, v jakém formátu chceme, aby byl náš soubor načten do paměti. Zde nám datová adresa URL vrátí něco takového:

Příklad Řetězec Base64

data:text/plain;base64,4oCcVGhlcmXigJlzIG5vIHJvb20gZm9yIHN1YnRsZXR5IG9uIHRoZSBpbnRlcm5ldC7igJ0g4oCUIEdlb3JnZSBIb3R6

I když to tak možná nevypadá, tento řetězec je schopen reprezentovat celý obsah souboru. Když naše reader plně načetl náš soubor do paměti, reader.onloadend je volána událost události a předává se do objektu onloadevent jako argument. Z tohoto objektu události můžeme získat přístup k datové URL představující obsah našeho souboru.

Než to uděláme, nastavíme volání na fetch() , předáním předpokládané adresy URL naší trasy nahrávání na serveru (když spustíte npm run dev ve standardu provozuje server na portu 5001 ). V objektu options pro fetch() ujistěte se, že jsme nastavili HTTP method na POST abychom mohli poslat tělo spolu s naší žádostí.

Také se ujistěte, že jsme nastavili režim cors na true, aby náš požadavek předal middleware CORS na serveru (to omezuje, jaké adresy URL mohou přistupovat k serveru – toto je předem nakonfigurováno tak, aby fungovalo mezi standardními verzemi Next.js a Node.js). Poté nastavíme také Content-Type header, což je standardní HTTP hlavička, která našemu serveru říká, v jakém formátu je naše POST tělo je uvnitř. Mějte na paměti, toto není stejný jako náš typ souboru.

V body pole, zavoláme na JSON.stringify()fetch() vyžaduje, abychom tělo předali jako řetězec, nikoli jako objekt – a k tomu předali objekt s daty, která budeme potřebovat na serveru k nahrání našeho souboru do S3.

Zde key je nastaven na file.name abychom zajistili, že soubor, který vložíme do bucketu S3, bude identický s názvem souboru vybraného z našeho počítače. contentType je nastaven na typ MIME, který nám byl automaticky poskytnut v objektu souboru prohlížeče (např. pokud jsme otevřeli .png soubor by byl nastaven na image/png ).

Důležitou součástí je zde data . Všimněte si, že používáme onLoadEndEvent jak jsme naznačili výše. Toto obsahuje obsah našeho souboru jako řetězec base64 v jeho target.result pole. Zde je volání na .split(',') na konci říká "rozdělte to na dva kusy, přičemž první je metadata o řetězci base64 a druhý je skutečný řetězec base64."

Musíme to udělat, protože pouze část za čárkou v naší datové adrese URL (viz příklad výše) je skutečný řetězec base64. Pokud neděláme vyjměte to, Amazon S3 uloží náš soubor, ale když jej otevřeme, bude nečitelný. Abychom dokončili tento řádek, použijeme notaci závorek pole, abychom řekli „dejte nám druhou položku v poli (pozice 1 v poli JavaScriptu založeném na nule)."

Tím je náš požadavek odeslán na server. Na závěr přidáme .then() zpětné volání – fetch vrátí nám příslib JavaScriptu – který potvrzuje úspěšné nahrávání a „resetuje“ naše uživatelské rozhraní. My setUploading() na false , vymažte <input /> a poté použijte pong upozorní knihovnu vestavěnou do standardu Next.js, aby zobrazila zprávu na obrazovce.

V případě, že dojde k selhání, uděláme totéž, ale namísto zprávy o úspěchu zobrazíme chybovou zprávu (pokud je k dispozici).

Pokud vše funguje podle plánu, měli bychom vidět něco takového:

Zabalit

V tomto tutoriálu jsme se naučili nahrávat soubory do Amazon S3 pomocí FileReader API v prohlížeči. Naučili jsme se, jak nastavit připojení k Amazon S3 přes aws-sdk a také jak vytvořit HTTP trasu, na kterou bychom mohli volat z klienta.

V prohlížeči jsme se naučili používat FileReader API pro převod našeho souboru na řetězec Base64 a poté použijte fetch() abychom předali náš soubor HTTP trase, kterou jsme vytvořili.