forked from sheetjs/docs.sheetjs.com
		
	
		
			
	
	
		
			247 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
		
		
			
		
	
	
			247 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
|  | --- | ||
|  | title: HTTP Server Processing | ||
|  | pagination_prev: demos/net/network | ||
|  | pagination_next: demos/net/email | ||
|  | --- | ||
|  | 
 | ||
|  | import current from '/version.js'; | ||
|  | import CodeBlock from '@theme/CodeBlock'; | ||
|  | 
 | ||
|  | Server-Side JS platforms like NodeJS and Deno have built-in APIs for listening | ||
|  | on network interfaces.  They provide wrappers for requests and responses. | ||
|  | 
 | ||
|  | ## Overview
 | ||
|  | 
 | ||
|  | #### Parsing Files in POST Requests
 | ||
|  | 
 | ||
|  | Typically servers receive form data with content type `multipart/form-data` or | ||
|  | `application/x-www-form-urlencoded`. The platforms themselves typically do not | ||
|  | provide "body parsing" functions, instead leaning on the community to supply | ||
|  | modules to take the encoded data and split into form fields and files. | ||
|  | 
 | ||
|  | NodeJS servers typically use a parser like `formidable`. In the example below, | ||
|  | `formidable` will write to file and `XLSX.readFile` will read the file: | ||
|  | 
 | ||
|  | ```js | ||
|  | var XLSX = require("xlsx"); // This is using the CommonJS build | ||
|  | var formidable = require("formidable"); | ||
|  | 
 | ||
|  | require("http").createServer(function(req, res) { | ||
|  |   if(req.method !== "POST") return res.end(""); | ||
|  | 
 | ||
|  |   /* parse body and implement logic in callback */ | ||
|  |   // highlight-next-line | ||
|  |   (new formidable.IncomingForm()).parse(req, function(err, fields, files) { | ||
|  |     /* if successful, files is an object whose keys are param names */ | ||
|  |     // highlight-next-line | ||
|  |     var file = files["upload"]; // <input type="file" id="upload" name="upload"> | ||
|  |     /* file.path is a location in the filesystem, usually in a temp folder */ | ||
|  |     // highlight-next-line | ||
|  |     var wb = XLSX.readFile(file.filepath); | ||
|  |     // print the first worksheet back as a CSV | ||
|  |     res.end(XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]])); | ||
|  |   }); | ||
|  | }).listen(process.env.PORT || 3000); | ||
|  | ``` | ||
|  | 
 | ||
|  | `XLSX.read` will accept NodeJS buffers as well as `Uint8Array`, Base64 strings, | ||
|  | binary strings, and plain Arrays of bytes.  This covers the interface types of | ||
|  | a wide variety of frameworks. | ||
|  | 
 | ||
|  | #### Writing Files in GET Requests
 | ||
|  | 
 | ||
|  | Typically server libraries use a response API that accepts `Uint8Array` data. | ||
|  | `XLSX.write` with the option `type: "buffer"` will generate data.  To force the | ||
|  | response to be treated as an attachment, set the `Content-Disposition` header: | ||
|  | 
 | ||
|  | ```js | ||
|  | var XLSX = require("xlsx"); // This is using the CommonJS build | ||
|  | 
 | ||
|  | require("http").createServer(function(req, res) { | ||
|  |   if(req.method !== "GET") return res.end(""); | ||
|  |   var wb = XLSX.read("S,h,e,e,t,J,S\n5,4,3,3,7,9,5", {type: "binary"}); | ||
|  |   // highlight-start | ||
|  |   res.setHeader('Content-Disposition', 'attachment; filename="SheetJS.xlsx"'); | ||
|  |   res.end(XLSX.write(wb, {type:"buffer", bookType: "xlsx"})); | ||
|  |   // highlight-end | ||
|  | }).listen(process.env.PORT || 3000); | ||
|  | ``` | ||
|  | 
 | ||
|  | ## NodeJS
 | ||
|  | 
 | ||
|  | When processing small files, the work is best handled in the server response | ||
|  | handler function.  This approach is used in the "Framework Demos" section. | ||
|  | 
 | ||
|  | When processing large files, the direct approach will freeze the server. NodeJS | ||
|  | provides ["Worker Threads"](#worker-threads) for this exact use case. | ||
|  | 
 | ||
|  | ### Framework Demos
 | ||
|  | 
 | ||
|  | #### Express
 | ||
|  | 
 | ||
|  | **[The exposition has been moved to a separate page.](/docs/demos/net/server/express)** | ||
|  | 
 | ||
|  | #### NestJS
 | ||
|  | 
 | ||
|  | **[The exposition has been moved to a separate page.](/docs/demos/net/server/nestjs)** | ||
|  | 
 | ||
|  | #### Fastify
 | ||
|  | 
 | ||
|  | **[The exposition has been moved to a separate page.](/docs/demos/net/server/fastify)** | ||
|  | 
 | ||
|  | ### Worker Threads
 | ||
|  | 
 | ||
|  | NodeJS "Worker Threads" were introduced in v14 and eventually marked as stable | ||
|  | in v16. Coupled with `AsyncResource`, a simple thread pool enables processing | ||
|  | without blocking the server! The official NodeJS docs include a sample worker | ||
|  | pool implementation. | ||
|  | 
 | ||
|  | This example uses ExpressJS to create a general XLSX conversion service, but | ||
|  | the same approach applies to any NodeJS server side framework. | ||
|  | 
 | ||
|  | When reading large files, it is strongly recommended to run the body parser in | ||
|  | the main server process. Body parsers like `formidable` will write uploaded | ||
|  | files to the filesystem, and the file path should be passed to the worker (and | ||
|  | the worker would be responsible for reading and cleaning up the files). | ||
|  | 
 | ||
|  | :::note pass | ||
|  | 
 | ||
|  | The `child_process` module can also spawn [command-line tools](/docs/demos/cli). | ||
|  | That approach is not explored in this demo. | ||
|  | 
 | ||
|  | ::: | ||
|  | 
 | ||
|  | <details><summary><b>Complete Example</b> (click to show)</summary> | ||
|  | 
 | ||
|  | :::note | ||
|  | 
 | ||
|  | This demo was last tested on 2023 August 27 with NodeJS 20.5.1 + ExpressJS | ||
|  | 4.18.2 + Formidable 2.1.1 | ||
|  | 
 | ||
|  | ::: | ||
|  | 
 | ||
|  | 0) Create a simple ECMAScript-Module-enabled `package.json`: | ||
|  | 
 | ||
|  | ```json title="package.json" | ||
|  | { "type": "module" } | ||
|  | ``` | ||
|  | 
 | ||
|  | 1) Install the dependencies: | ||
|  | 
 | ||
|  | <CodeBlock language="bash">{`\ | ||
|  | npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz express@4.18.2 formidable@2.1.1`} | ||
|  | </CodeBlock> | ||
|  | 
 | ||
|  | 2) Create a worker script `worker.js` that listens for messages. When a message | ||
|  | is received, it will read the file from the filesystem, generate and pass back a | ||
|  | new XLSX file, and delete the original file: | ||
|  | 
 | ||
|  | ```js title="worker.js" | ||
|  | /* load the worker_threads module */ | ||
|  | import { parentPort } from 'node:worker_threads'; | ||
|  | 
 | ||
|  | /* load the SheetJS module and hook to FS */ | ||
|  | import { set_fs, readFile, write } from 'xlsx'; | ||
|  | import * as fs from 'fs'; | ||
|  | set_fs(fs); | ||
|  | 
 | ||
|  | /* the server will send a message with the `path` field */ | ||
|  | parentPort.on('message', (task) => { | ||
|  |   /* highlight-start */ | ||
|  |   // read file | ||
|  |   const wb = readFile(task.path, { dense: true }); | ||
|  |   // send back XLSX | ||
|  |   parentPort.postMessage(write(wb, { type: "buffer", bookType: "xlsx" })); | ||
|  |   /* highlight-end */ | ||
|  |   // remove file | ||
|  |   fs.unlink(task.path, ()=>{}); | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | 3) Download [`worker_pool.js`](pathname:///server/worker_pool.js): | ||
|  | 
 | ||
|  | ```bash | ||
|  | curl -LO https://docs.sheetjs.com/server/worker_pool.js | ||
|  | ``` | ||
|  | 
 | ||
|  | (this is a slightly modified version of the example in the NodeJS docs) | ||
|  | 
 | ||
|  | 4) Save the following server code to `main.mjs`: | ||
|  | 
 | ||
|  | ```js title="main.mjs" | ||
|  | /* load dependencies */ | ||
|  | import os from 'node:os'; | ||
|  | import process from 'node:process' | ||
|  | import express from 'express'; | ||
|  | import formidable from 'formidable'; | ||
|  | 
 | ||
|  | /* load worker pool */ | ||
|  | import WorkerPool from './worker_pool.js'; | ||
|  | 
 | ||
|  | const pool = new WorkerPool(os.cpus().length); | ||
|  | process.on("beforeExit", () => { pool.close(); }) | ||
|  | 
 | ||
|  | /* create server */ | ||
|  | const app = express(); | ||
|  | app.post('/', (req, res, next) => { | ||
|  |   // parse body | ||
|  |   const form = formidable({}); | ||
|  |   form.parse(req, (err, fields, files) => { | ||
|  |     // look for "upload" field | ||
|  |     if(err) return next(err); | ||
|  |     if(!files["upload"]) return next(new Error("missing `upload` file")); | ||
|  | 
 | ||
|  |     // send a message to the worker with the path to the uploaded file | ||
|  |     // highlight-next-line | ||
|  |     pool.runTask({ path: files["upload"].filepath }, (err, result) => { | ||
|  |       if(err) return next(err); | ||
|  |       // send the file back as an attachment | ||
|  |       res.attachment("SheetJSPool.xlsx"); | ||
|  |       res.status(200).end(result); | ||
|  |     }); | ||
|  |   }); | ||
|  | }); | ||
|  | 
 | ||
|  | // start server | ||
|  | app.listen(7262, () => { console.log(`Example app listening on port 7262`); }); | ||
|  | ``` | ||
|  | 
 | ||
|  | 5) Run the server: | ||
|  | 
 | ||
|  | ```bash | ||
|  | node main.mjs | ||
|  | ``` | ||
|  | 
 | ||
|  | Test with the [`pres.numbers` sample file](https://sheetjs.com/pres.numbers): | ||
|  | 
 | ||
|  | ```bash | ||
|  | curl -LO https://sheetjs.com/pres.numbers | ||
|  | curl -X POST -F upload=@pres.numbers http://localhost:7262/ -J -O | ||
|  | ``` | ||
|  | 
 | ||
|  | This will generate `SheetJSPool.xlsx`. | ||
|  | 
 | ||
|  | </details> | ||
|  | 
 | ||
|  | ## Other Platforms
 | ||
|  | 
 | ||
|  | ### Deno
 | ||
|  | 
 | ||
|  | :::caution pass | ||
|  | 
 | ||
|  | Many hosted services like Deno Deploy do not offer filesystem access. | ||
|  | 
 | ||
|  | This breaks web frameworks that use the filesystem in body parsing. | ||
|  | 
 | ||
|  | ::: | ||
|  | 
 | ||
|  | Deno provides the basic elements to implement a web server.  It does not provide | ||
|  | a body parser out of the box. | ||
|  | 
 | ||
|  | #### Drash
 | ||
|  | 
 | ||
|  | In testing, [Drash](https://drash.land/drash/) had an in-memory body parser | ||
|  | which could handle file uploads on hosted services like Deno Deploy. | ||
|  | 
 | ||
|  | **[The exposition has been moved to a separate page.](/docs/demos/net/server/drash)** |