diff --git a/README.md b/README.md
index 0a6c63e..b32b3ca 100644
--- a/README.md
+++ b/README.md
@@ -1,110 +1,81 @@
-# ๐ **Faster**
-
-> [!IMPORTANT]\
-> **Please give a star!** โญ
-
----
-
-## ๐ Introduction
-
-**Faster** is a **fast and optimized middleware server** with an incredibly
-small codebase (~300 lines), built on top of native HTTP APIs **with no
-dependencies**. It includes a collection of useful middlewares (Some are
-specific to Deno):
-
-- ๐ **Log file**
-- ๐๏ธ **Serve static**
-- ๐ **CORS**
-- ๐ **Session**
-- โฑ๏ธ **Rate limit**
-- ๐ก๏ธ **Token**
-- ๐ฅ **Body parsers**
-- ๐ **Redirect**
-- ๐ **Proxy**
-- ๐ค **Handle upload**
-
-Fully compatible with **Deno Deploy** and other environments. Examples of all
-resources are available in this README. Faster's ideology is simple: all you
-need is an optimized middleware manager; all other functionality is middleware.
-
----
-
-## ๐ **Contents**
-
-- [โก Benchmarks](#-benchmarks)
-- [๐ Example](#-example)
- - [๐ฃ๏ธ Defining Routes](#%EF%B8%8F-defining-routes)
- - [๐จ POST: Read and Return JSON](#-post-read-and-return-json)
- - [๐ GET: Return HTML](#-get-return-html)
- - [๐ Get URL Params](#-get-url-params)
- - [๐ช Cookies](#-cookies)
- - [โฉ๏ธ Redirect](#%EF%B8%8F-redirect)
- - [๐ฌ WebSockets](#-websockets)
-- [๐ ๏ธ Middlewares](#%EF%B8%8F-middlewares)
- - [๐ฆ Set Deno KV and Deno KV FS](#-set-deno-kv-and-deno-kv-fs)
- - [๐ Logger](#-logger)
- - [๐ฅ Body Parsers (`res` and `req`)](#-body-parsers-res-and-req)
- - [โฑ๏ธ Rate Limit](#%EF%B8%8F-rate-limit)
- - [๐๏ธ Serve Static](#%EF%B8%8F-serve-static)
- - [๐ Set CORS](#-set-cors)
- - [๐ Token](#-token)
- - [โฉ๏ธ Redirect Middleware](#%EF%B8%8F-redirect-middleware)
- - [๐ Session](#-session)
- - [๐ Proxy](#-proxy)
- - [๐ค Upload](#-upload)
- - [๐ Upload Usage](#-upload-usage)
- - [๐ป Upload Examples in Frontend and Backend](#-upload-examples-in-frontend-and-backend)
-- [๐ Organizing Routes in Files](#-organizing-routes-in-files)
-- [๐ฆ All Imports](#-all-imports)
-- [๐ Example Deploy in Ubuntu](#-example-deploy-in-ubuntu)
- - [๐ ๏ธ Create Service](#%EF%B8%8F-create-service)
- - [๐ Configure HTTPS](#-configure-https)
-- [๐ก See Also: Faster with React](#-see-also-faster-with-react)
-- [๐จโ๐ป About](#-about)
-
----
-
-## โก **Benchmarks**
-
-The middleware is built on top of Deno's native HTTP APIs. See the benchmarks
-(for a 'Hello World' server):
-
-**Machine**: 8 GiB RAM, Intelยฎ Coreโข i5-10210U CPU @ 2.11GHz ร 4\
-**Method**: `autocannon -c 100 -d 40 -p 10 localhost:80`\
-**Environment**: Deno v1.46.3, Ubuntu 24.04 LTS
-
-| Framework | Version | Router? | Results |
-| ---------- | :------: | :-----: | ----------------------------------------- |
-| Express | 4.19.2 | โ | 167k requests in 40.11s, **29 MB** read |
-| Fastify | 4.28.1 | โ | 1105k requests in 40.07s, **193 MB** read |
-| Oak | 17.0.0 | โ | 260k requests in 40.09s, **45 MB** read |
-| **Faster** | **12.1** | **โ** | **1432k requests in 40.17s, 250 MB read** |
-
-> **Note:** In addition to its performance, Faster is a very complete framework
-> considering its middleware collection.
-
----
-
-## ๐ **Example**
-
-### ๐ฃ๏ธ **Defining Routes**
-
-- **Static Routes**: `/foo`, `/foo/bar`
-- **Parameterized Routes**:
- - Simple: `/:title`, `/books/:title`, `/books/:genre/:title`
- - With Suffix: `/movies/:title.mp4`, `/movies/:title.(mp4|mov)`
- - Optional Parameters: `/:title?`, `/books/:title?`, `/books/:genre/:title?`
-- **Wildcards**: `*`, `/books/*`, `/books/:genre/*`
-
----
-
-### ๐จ **POST: Read and Return JSON**
+# Faster
+
+A fast and optimized middleware server with an absurdly small amount of code
+(300 lines) built on top of Deno's native HTTP APIs with no dependencies. It
+also has a collection of useful middlewares: log file, serve static, CORS,
+session, rate limit, token, body parsers, redirect, proxy and handle upload. In
+"README" there are examples of all the resources. Faster's ideology is: all you
+need is an optimized middleware manager, all other functionality is middleware.
+
+## Contents
+
+- [Benchmarks](#benchmarks)
+
+- [Example](#example)
+ - [Defining routes](#defining-routes)
+ - [POST read and return JSON](#post-read-and-return-json)
+ - [GET return HTML](#get-return-html)
+ - [Get URL params](#get-url-params)
+ - [Cookies](#cookies)
+ - [Redirect](#redirect)
+- [Middleares](#middleares)
+ - [Logger](#logger)
+ - [Body Parsers res and req](#body-Parsers-res-and-req)
+ - [Rate Limit](#rate-limit)
+ - [Serve Static](#serve-static)
+ - [Set Cors](#set-cors)
+ - [Token](#token)
+ - [Redirect](#redirect)
+ - [Session](#session)
+ - [Proxy](#proxy)
+ - [Upload](#upload)
+ - [Upload usage](#upload-usage)
+ - [Upload examples in frontend and backend](#upload-examples-in-frontend-and-backend)
+- [All imports](#all-imports)
+- [Example Deploy](#example-deploy)
+ - [Create service](#create-service)
+ - [Configure HTTPS](#configure-https)
+- [About](#about)
+
+## Benchmarks
+
+The middleware is built on top of Deno's native HTTP APIs, see the benchmarks
+('hello word' server):
+
+**Machine**: 8 GiB, Intelยฎ Coreโข i5-10210U CPU @ 2.11GHz ร 4
+
+**method**: `autocannon -c 100 -d 40 -p 10 localhost:80`. Deno v1.19.3, Ubuntu
+20.04 LTS.
+
+| Framework | Version | Router? | Results |
+| ---------- | :-----: | :-----: | ----------------------------------------: |
+| Express | 4.17.3 | โ | 167k requests in 40.11s, 29 MB read |
+| Fastify | 3.27.4 | โ | 1105k requests in 40.07s ,193 MB read |
+| Oak | 10.4.0 | โ | 260k requests in 40.09s, 45 MB read |
+| **Faster** | **5.7** | **โ** | **1432k requests in 40.17s, 250 MB read** |
+
+Note that in addition to performance, Faster is a very complete framework
+considering its middleware collection.
+
+## Example
+
+### Defining routes
+
+Static (/foo, /foo/bar)
+
+Parameter (/:title, /books/:title, /books/:genre/:title)
+
+Parameter w/ Suffix (/movies/:title.mp4, /movies/:title.(mp4|mov))
+
+Optional Parameters (/:title?, /books/:title?, /books/:genre/:title?)
+
+Wildcards (\*, /books/\*, /books/:genre/\*)
+
+### POST read and return JSON
```typescript
import { req, res, Server } from "https://deno.land/x/faster/mod.ts";
-
const server = new Server();
-
server.post(
"/example_json",
res("json"),
@@ -115,16 +86,10 @@ server.post(
await next();
},
);
-
await server.listen({ port: 80 });
-
-//or with the portable command "serve":
-export default { fetch: server.fetch };
```
----
-
-### ๐ **GET: Return HTML**
+### GET return HTML
```typescript
server.get(
@@ -136,11 +101,11 @@ server.get(
- Title Example
+ title example
-
- HTML body example
+ HTML body example
+
`;
await next();
@@ -148,39 +113,33 @@ server.get(
);
```
----
-
-### ๐ **Get URL Params**
+### Get URL params
```typescript
server.get(
"/example_params/:ex1?foo=bar",
async (ctx: any, next: any) => {
console.log(ctx.params.ex1);
- console.log(ctx.url.searchParams.get("foo")); // Explore the URL (ctx.url) object
+ console.log(ctx.url.searchParams.get("foo")); //you can explore the URL (ctx.url) object
await next();
},
);
```
----
-
-### ๐ช **Cookies**
+### Cookies
```typescript
import {
Cookie,
deleteCookie,
getCookies,
- getSetCookies,
Server,
setCookie,
-} from "https://deno.land/x/faster/mod.ts"; // Alias to Deno std
-
+} from "https://deno.land/x/faster/mod.ts"; //alias to deno std
server.get(
"/cookies",
async (ctx: any, next: any) => {
- setCookie(ctx.res.headers, { name: "user_name", value: "San" }); // Explore interface 'Cookie' for more options
+ setCookie(ctx.res.headers, { name: "user_name", value: "San" }); //explore interface 'Cookie' for more options
deleteCookie(ctx.res.headers, "last_order");
console.log(getCookies(ctx.req.headers));
await next();
@@ -188,24 +147,11 @@ server.get(
);
```
----
-
-### โฉ๏ธ **Redirect**
-
-Use: `ctx.redirect([status,] "/my_custom_url_or_path")`. The default status is
-`302`.
+### Redirect
```typescript
server.get(
"/redirect_example",
- async (ctx: any, next: any) => {
- ctx.redirect(303, "/my_custom_url_or_path");
- await next();
- },
-);
-
-server.get(
- "/redirect_example2",
async (ctx: any, next: any) => {
ctx.redirect("/my_custom_url_or_path");
await next();
@@ -213,164 +159,86 @@ server.get(
);
```
----
-
-### ๐ฌ **WebSockets**
-
-By default, the server will reject WebSocket connections to prevent
-vulnerabilities. To accept connections, use the `acceptOrRejectSocketConn`
-function, which should return an ID to retrieve the WebSocket later. If the
-function returns `undefined`, `""`, `null`, `0`, etc., the connection will be
-rejected.
-
-**Example:**
-
-```typescript
-server.acceptOrRejectSocketConn = async (ctx: Context) => {
- // Returning undefined, "", null, or 0 will reject the connection.
- return ctx.req.headers.get("Host")!; // Return ID
-};
-```
-
-**Retrieving the Socket by ID:**
-
-```typescript
-server.openedSockets.get(yourId); // As in the example, ctx.req.headers.get("Host")!
-```
-
-**Receiving WebSocket Events:**
-
-```typescript
-server.onSocketMessage = async (id: string, socket: WebSocket, event: any) => {
- console.log(id);
- console.log(socket);
- console.log(event);
-};
-
-server.onSocketClosed = async (id: string, socket: WebSocket) => {
- console.log(id);
- console.log(socket);
-};
-//... server.onSocketError, server.onSocketOpen
-```
-
----
-
-## ๐ ๏ธ **Middlewares**
-
-This project has a standard set of middlewares useful for most cases.
-
-### ๐ฆ **Set Deno KV and Deno KV FS**
-
-You need to launch Deno KV and Deno KV FS as several middlewares depend on it.
-
-```typescript
-const kv = await Deno.openKv(); // Use your parameters here to launch a custom Deno.Kv
-Server.setKv(kv);
-```
-
-Now, you can globally access instances in `Server.kv` and `Server.kvFs`.
-
-- **Deno KV File System (`Server.kvFs`):** Compatible with Deno Deploy. Saves
- files in 64KB chunks. You can organize files into directories, control the
- KB/s rate for saving and reading files, impose rate limits, set user space
- limits, and limit concurrent operationsโuseful for controlling
- uploads/downloads. Utilizes the Web Streams API.
-
-See more at: [deno_kv_fs](https://github.com/hviana/deno_kv_fs)
-
----
+## Middleares
-### ๐ **Logger**
+This project has a standard set of middleware useful for most cases.
-```typescript
-logger(save: boolean = true, print: boolean = true)
-```
+### Logger
-**Initialize Deno KV (if not already done):**
+Example:
```typescript
-const kv = await Deno.openKv();
-Server.setKv(kv);
+server.use(logger());
```
-**Usage:**
+You can pass custom log file:
```typescript
-// You can also use useAtBeginning
-server.use(logger()); // With default options: save and print are true
+logger("./my_dir/my_custom_log.txt");
```
-**Access Log Data:**
-
-- **Retrieve Logs:** `await FasterLog.get(startMillis, endMillis)`
-- **Delete Logs:** `await FasterLog.delete(startMillis, endMillis)`
+### Body Parsers res and req
----
-
-### ๐ฅ **Body Parsers (`res` and `req`)**
-
-**Example:**
+Example:
```typescript
server.post(
"/example_parsers",
- res("json"), // Response parser
- req("json"), // Request parser
+ res("json"), //Response parser
+ req("json"), //Request parser
async (ctx: any, next: any) => {
- console.log(ctx.body); // The original (unparsed) body is in ctx.req.body
+ console.log(ctx.body); //the original (no parser) body is in ctx.req.body
ctx.res.body = { msg: "json response example" };
await next();
},
);
```
-**Supported Options:**
+The current supported options for "req" are: "arrayBuffer", "blob", "formData",
+"json", "text".
-- **`req` Parsers:** `"arrayBuffer"`, `"blob"`, `"formData"`, `"json"`, `"text"`
-- **`res` Parsers:** `"json"`, `"html"`, `"javascript"`
+The current supported options for "res" are: "json", "html", "javascript".
-**Custom Parsing Example:**
+If there are no parsers for your data, don't worry, you can handle the data
+manually, Ex:
```typescript
server.post(
- "/custom_parse",
+ "/upload",
async (ctx: any, next: any) => {
- ctx.res.headers.set("Content-Type", "application/json");
- const data = await customParseBody(ctx.req.body); // Handle ctx.req.body manually
- ctx.res.body = JSON.stringify({ msg: "ok" });
+ ctx.res.headers.set(
+ "Content-Type",
+ "application/json",
+ );
+ const data = await exCustomParseBody(ctx.req.body); //do what you want with ctx.req.body
+ ctx.res.body = JSON.stringify({ msg: "ok" }); // //ctx.res.body can also be other data types such as streams, bytes and etc.
await next();
},
);
```
----
-
-### โฑ๏ธ **Rate Limit**
+### Rate Limit
-**Usage:**
+Example:
```typescript
-// You can also use useAtBeginning
server.use(rateLimit());
```
-**Options (with default values):**
+OPTIONS (with default values):
```typescript
rateLimit({
attempts: 30,
interval: 10,
maxTableSize: 100000,
- id: (ctx: Context) => ctx.req.headers.get("Host")!,
+ id: (ctx: Context) => JSON.stringify(ctx.conn.remoteAddr),
});
```
----
+### Serve Static
-### ๐๏ธ **Serve Static**
-
-**Example (route must end with `/*`):**
+Example (must end with "/*"):
```typescript
server.get(
@@ -379,15 +247,12 @@ server.get(
);
```
----
-
-### ๐ **Set CORS**
+### Set Cors
-**Example:**
+Example:
```typescript
-server.options("/example_cors", setCORS()); // Enable pre-flight request
-
+server.options("/example_cors", setCORS()); //enable pre-fligh request
server.get(
"/example_cors",
setCORS(),
@@ -397,25 +262,21 @@ server.get(
);
```
-**Specify Allowed Hosts:**
+You can pass valid hosts to cors function:
```typescript
setCORS("http://my.custom.url:8080");
```
----
-
-### ๐ **Token**
+### Token
This middleware is encapsulated in an entire static class. It uses Bearer Token
-and default options with the "HS256" algorithm, generating a random secret when
-starting the application (you can also set a secret manually).
-
-**Usage:**
+and default options with the "HS256" algorithm, and generates a random secret
+when starting the application (you can also set a secret manually). Ex:
```typescript
server.get(
- "/example_verify_token", // Send token to server in Header => Authorization: Bearer TOKEN
+ "/example_verify_token", //send token to server in Header => Authorization: Bearer TOKEN
Token.middleware,
async (ctx, next) => {
console.log(ctx.extra.tokenPayload);
@@ -425,151 +286,104 @@ server.get(
);
```
-**Generate Token:**
-
-```typescript
-await Token.generate({ user_id: "172746" }, null); // Null for never expire; defaults to "1h"
-```
-
-**Set Secret:**
+Generate Token ex:
```typescript
-Token.setSecret("a3d2r366wgb3dh6yrwzw99kzx2"); // Do this at the beginning of your application
+await Token.generate({ user_id: "172746" }, null); //null to never expire, this parameter defaults to "1h"
```
-**Get Token Payload Outside Middleware:**
+Set secret ex:
```typescript
-await Token.getPayload("YOUR_TOKEN_STRING"); // For example, to get token data from token string in URL parameter
+Token.setSecret("a3d2r366wgb3dh6yrwzw99kzx2"); //Do this at the beginning of your application
```
-**Set Configurations:**
+Get token payload out of middleware:
```typescript
-Token.setConfigs(/* your configurations */);
+await Token.getPayload("YOUR_TOKEN_STRING"); //Ex: use for get token data from token string in URL parameter.
```
----
+You can also use the static method `Token.setConfigs`.
-### โฉ๏ธ **Redirect Middleware**
+### Redirect
-**Usage:** `redirect([status,] "/my_custom_url_or_path")`. The default status is
-`302`.
-
-**Example:**
+Ex:
```typescript
server.get(
"/my_url_1",
- redirect(303, "/my_url_2"), // Or the full URL
-);
-
-server.get(
- "/my_url_2",
- redirect("/my_url_3"), // Or the full URL
+ redirect("/my_url_2"), //or the full url
);
```
----
-
-### ๐ **Session**
-
-**Initialize Deno KV (if not already done):**
-
-```typescript
-const kv = await Deno.openKv();
-Server.setKv(kv);
-```
+### Session
-#### **Example**
+Ex:
```typescript
-// You can also use useAtBeginning
server.use(session());
-
-// In routes:
+//in routes:
server.get(
"/session_example",
async (ctx, next) => {
- console.log(ctx.extra.session); // Get session data
- ctx.extra.session.value.foo = "bar"; // Set session data (foo => "bar")
+ console.log(ctx.extra.session); //get session data
+ ctx.extra.session.foo = "bar"; //set session data
await next();
},
);
```
-- The default engine uses Deno KV and is optimized.
-
-#### **Expiration Policies**
-
-- **Absolute Expiration:** The object in the cache will expire after a certain
- time from when it was inserted, regardless of its usage. A value of `0`
- disables this expiration.
-- **Sliding Expiration:** The object expires after a configured time from the
- last request (`get` or `set`). A value of `0` disables this expiration.
-
-**Note:** If both `slidingExpiration` and `absoluteExpiration` are `0`,
-expiration is disabled. If both are greater than `0`, `absoluteExpiration`
-cannot be less than `slidingExpiration`.
-
-**Session Storage Engine Interface:**
+OPTIONS (with default values):
-```typescript
-constructor(
- slidingExpiration: number = 0,
- absoluteExpiration: number = 0
-)
```
-
-**Default Values:**
-
-```typescript
-session(engine: SessionStorageEngine = new KVStorageEngine()) // Default is 60 min slidingExpiration
+session(engine: SessionStorageEngine = new SQLiteStorageEngine(60)) //60 is 60 minutes to expire session
```
----
-
-### ๐ **Proxy**
+### Proxy
-**Usage:**
+Ex:
```typescript
-// You can also use useAtBeginning
server.use(proxy({ url: "https://my-url-example.com" }));
-server.use(proxy({ url: async (ctx) => "https://my-url-example.com" }));
```
-**In Routes:**
+In routes:
```typescript
server.get(
"/proxy_example",
async (ctx, next) => {
- console.log(ctx.req); // Request points to the proxy
- console.log(ctx.res); // Response contains the proxy answer
+ console.log(ctx.req); //req has changed as it now points to the proxy
+ console.log(ctx.res); //res has changed because now it has the proxy answer
+
+ //OR if replaceReqAndRes = false
+ console.log(ctx.extra.proxyReq);
+ console.log(ctx.extra.proxyRes);
+
await next();
},
);
```
-**Specific Proxy Route:**
+Or proxy in specific route:
```typescript
server.get(
"/proxy_example",
proxy({
url: "https://my-url-example.com/proxy_ex2",
- replaceProxyPath: false, // Specific proxy route for "/proxy_example"
+ replaceProxyPath: false, //specific proxy route for the route "/proxy_example"
}),
async (ctx, next) => {
- console.log(ctx.req);
- console.log(ctx.res);
+ console.log(ctx.req); //req has changed as it now points to the proxy
+ console.log(ctx.res); //res has changed because now it has the proxy answer
await next();
},
);
```
-**Conditional Proxy:**
+Conditional proxy:
```typescript
server.get(
@@ -577,314 +391,227 @@ server.get(
proxy({
url: "https://my-url-example.com/proxy_ex3",
condition: (ctx) => {
- return ctx.url.searchParams.get("foo") ? true : false;
+ if (ctx.url.searchParams.get("foo")) {
+ return true;
+ } else {
+ return false;
+ }
},
}),
async (ctx, next) => {
- console.log(ctx.extra.proxied); // True if proxy condition is true
- console.log(ctx.req);
- console.log(ctx.res);
+ console.log(ctx.extra.proxied); //will be true if proxy condition is true
+ console.log(ctx.req); //req has changed as it now points to the proxy
+ console.log(ctx.res); //res has changed because now it has the proxy answer
await next();
},
);
```
-**Options (with default values):**
+OPTIONS (with default values):
-```typescript
-proxy({
- url: string,
- replaceReqAndRes: true,
- replaceProxyPath: true,
- condition: (ctx: Context) => true,
-});
+```
+proxy(url: string, replaceReqAndRes: true, replaceProxyPath: true, condition: : (ctx: Context) => true )
```
-> **Warning:** Do not use "res body parsers" with `replaceReqAndRes: true`
-> (default)!\
-> **Note:** If you don't use Request body information before the proxy or in
-> your condition, avoid using "req body parsers" to reduce processing cost.
+**Do not use "res body parsers" with 'replaceReqAndRes: true' (default) !!!**
----
+**If you don't use Request body information before the proxy or in your
+condition, don't use "req body parsers" as this will increase the processing
+cost !!!**
-### ๐ค **Upload**
+### Upload
-**Initialize Deno KV (if not already done):**
+This middleware automatically organizes uploads to avoid file system problems
+and create dirs if not exists, perform validations and optimizes ram usage when
+uploading large files using Deno standard libraries!
-```typescript
-const kv = await Deno.openKv();
-Server.setKv(kv);
-```
+#### Upload usage
-This middleware uses Deno KV File System
-([deno_kv_fs](https://github.com/hviana/deno_kv_fs)).
+Ex:
-#### ๐ **Upload Usage**
+```typescript
+.post("/upload", upload(), async (ctx: any, next: any) => { ...
+```
-**Example:**
+Ex (with custom options):
```typescript
-// The route must end with *
-server.post("/files/*", upload(), async (ctx: any, next: any) => {/* ... */});
-server.get("/files/*", download(), async (ctx: any, next: any) => {/* ... */});
+.post("/upload", upload({ path: 'uploads_custom_dir' , extensions: ['jpg', 'png'], maxSizeBytes: 20000000, maxFileSizeBytes: 10000000, saveFile: true, readFile: false, useCurrentDir: true }), async (ctx: any, next: any) => { ...
```
-**With Custom Options:**
+Request must contains a body with form type "multipart/form-data", and inputs
+with type="file".
-- **Download:**
+Ex (pre validation):
-```typescript
-server.post(
- "/files/*",
- upload({
- allowedExtensions: async (ctx: Context) => ["jpg"],
- maxSizeBytes: async (ctx: Context) =>
- (ctx.extra.user.isPremium() ? 1 : 0.1) * 1024 * 1024 * 1024, // 1GB or 100MB
- maxFileSizeBytes: async (ctx: Context) =>
- (ctx.extra.user.isPremium() ? 1 : 0.1) * 1024 * 1024 * 1024, // 1GB or 100MB
- chunksPerSecond: async (ctx: Context) =>
- (ctx.extra.user.isPremium() ? 10 : 1) /
- kvFs.getClientReqs(ctx.extra.user.id),
- maxClientIdConcurrentReqs: async (
- ctx: Context,
- ) => (ctx.extra.user.isPremium() ? 10 : 1),
- clientId: async (ctx: Context) => ctx.extra.user.id,
- validateAccess: async (ctx: Context, path: string[]) =>
- ctx.extra.user.hasDirAccess(path),
- }),
- async (ctx: any, next: any) => {/* ... */},
-);
+```javascript
+.post("/pre_upload", preUploadValidate(["jpg", "png"], 20000000, 10000000), async (ctx: any, next: any) => { ...
```
-- **Upload:**
+Pre validation options:
-```typescript
-server.get(
- "/files/*",
- download({
- chunksPerSecond: async (ctx: Context) =>
- (ctx.extra.user.isPremium() ? 10 : 1) /
- kvFs.getClientReqs(ctx.extra.user.id),
- maxClientIdConcurrentReqs: async (
- ctx: Context,
- ) => (ctx.extra.user.isPremium() ? 10 : 1),
- clientId: async (ctx: Context) => ctx.extra.user.id,
- validateAccess: async (ctx: Context, path: string[]) =>
- ctx.extra.user.hasDirAccess(path),
- maxDirEntriesPerSecond: async (
- ctx: Context,
- ) => (ctx.extra.user.isPremium() ? 1000 : 100),
- pagination: async (ctx: Context) => true,
- cursor: async (ctx: Context) => ctx.url.searchParams.get("cursor"),
- }),
-);
+```
+preUploadValidate(
+ extensions: Array = [],
+ maxSizeBytes: number = Number.MAX_SAFE_INTEGER,
+ maxFileSizeBytes: number = Number.MAX_SAFE_INTEGER,
+)
```
-#### ๐ป **Upload Examples in Frontend and Backend**
+#### Upload examples in frontend and backend
-**Frontend (AJAX with multiple files):**
+Below an frontend example to work with AJAX, also accepting type="file"
+multiple:
```javascript
-const files = document.querySelector("#yourFormId input[type=file]").files;
-const name = document.querySelector("#yourFormId input[type=file]")
- .getAttribute("name");
+var files = document.querySelector("#yourFormId input[type=file]").files;
+var name = document.querySelector("#yourFormId input[type=file]").getAttribute(
+ "name",
+);
-const form = new FormData();
-for (let i = 0; i < files.length; i++) {
+var form = new FormData();
+for (var i = 0; i < files.length; i++) {
form.append(`${name}_${i}`, files[i]);
}
-const userId = 1; // Example
-const res = await fetch(`/files/${userId}`, {
+var res = await fetch("/upload", { //Fetch API automatically puts the form in the format "multipart/form-data".
method: "POST",
body: form,
}).then((response) => response.json());
-
console.log(res);
+
+//VALIDATIONS --------------
+
+var validationData = {};
+for (var i = 0; i < files.length; i++) {
+ var newObj = { //newObj is needed, JSON.stringify(files[i]) not work
+ "name": files[i].name,
+ "size": files[i].size,
+ };
+ validationData[`${name}_${i}`] = newObj;
+}
+var validations = await fetch("/pre_upload", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(validationData),
+}).then((response) => response.json());
+console.log(validations);
```
-**Backend (Deno):**
+In Deno (backend):
```typescript
import {
- download,
+ preUploadValidate,
res,
Server,
upload,
} from "https://deno.land/x/faster/mod.ts";
-
const server = new Server();
-
server.post(
- "/files/*", // For example: /files/general/myFile.xlsx
+ "/upload",
res("json"),
- upload(), // Using default options. No controls.
+ upload({
+ path: "my_uploads",
+ extensions: ["jpg", "png"],
+ maxSizeBytes: 20000000,
+ maxFileSizeBytes: 10000000,
+ }),
async (ctx: any, next: any) => {
ctx.res.body = ctx.extra.uploadedFiles;
await next();
},
);
-
-server.get(
- "/files/*",
- download(), // Using default options. No controls.
+server.post(
+ "/pre_upload",
+ res("json"),
+ preUploadValidate(["jpg", "png"], 20000000, 10000000),
+ async (ctx: any, next: any) => {
+ ctx.res.body = { msg: "Passed upload validations." };
+ await next();
+ },
);
-
server.get("/", res("html"), async (ctx: any, next: any) => {
ctx.res.body = `
-
- `;
+
+ `;
await next();
});
-
-await server.listen({ port: 80 });
-
-//or with the portable command "serve":
-export default { fetch: server.fetch };
-```
-
----
-
-## ๐ **Organizing Routes in Files**
-
-It's possible to organize routes into files using native JavaScript resources.
-
-**Main File:**
-
-```typescript
-import { Server } from "https://deno.land/x/faster/mod.ts";
-import exampleRoutes from "./example_routes.ts";
-
-const server = new Server();
-exampleRoutes("example", server);
-
await server.listen({ port: 80 });
-
-//or with the portable command "serve":
-export default { fetch: server.fetch };
-```
-
-**Secondary Route File (`example_routes.ts`):**
-
-```typescript
-import { req, res, Server } from "https://deno.land/x/faster/mod.ts";
-
-export default function exampleRoutes(namespace: string, server: Server) {
- server.post(
- `${namespace}/json`,
- res("json"),
- req("json"),
- async (ctx: any, next: any) => {
- console.log(ctx.body);
- ctx.res.body = { msg: "json response example" };
- await next();
- },
- );
-
- server.get(
- `${namespace}/html`,
- res("html"),
- async (ctx: any, next: any) => {
- ctx.res.body = `
-
-
-
-
- Title Example
-
-
- HTML body example
-
-
- `;
- await next();
- },
- );
-}
```
----
-
-## ๐ฆ **All Imports**
+## All imports
```typescript
import {
Context,
- ContextResponse, // Type
- Cookie, // Type, alias to Deno std
- deleteCookie, // Alias to Deno std
- download,
- FasterLog,
- getCookies, // Alias to Deno std
- getSetCookies, // Alias to Deno std
- KVStorageEngine,
+ ContextResponse, //type
+ Cookie, //type, alias to deno std
+ deleteCookie, //alias to deno std
+ getCookies, //alias to deno std
logger,
- NextFunc, // Type
- Params, // Type
+ NextFunc, //type
+ Params, //type
parse,
- ProcessorFunc, // Type
+ preUploadValidate,
+ ProcessorFunc, //type
proxy,
rateLimit,
redirect,
req,
res,
- Route, // Type
- RouteFn, // Type
+ Route, //type
+ RouteFn, //type
Server,
serveStatic,
- Session, // Type
+ Session, //type
session,
SessionStorageEngine,
- setCookie, // Alias to Deno std
+ setCookie, //alias to deno std
setCORS,
+ SQLiteStorageEngine,
Token,
upload,
-} from "jsr:@hviana/faster";
-import * as jose from "jsr:@hviana/faster/jose"; // jsr port of deno panva/jose (v6.0.8)
-import * as deno_kv_fs from "jsr:@hviana/faster/deno-kv-fs"; // Alias to jsr @hviana/deno-kv-fs (v1.0.1)
+} from "https://deno.land/x/faster/mod.ts";
```
----
-
-## ๐ **Example Deploy in Ubuntu**
+## Example Deploy
-Example of deploying an application named "my-deno-app" in a Ubuntu environment.
-Change "my-deno-app" and directories to yours.
+Example of depoly application "my-deno-app" in ubuntu environment. Change the
+"my-deno-app" and the directories to yours.
-### ๐ ๏ธ **Create Service**
+### Create service
-**Create Run Script ("run-server.sh") in Your Application Folder:**
+Create run script ("run-server.sh") in your application folder with the content:
-```bash
+```
#!/bin/bash
-/home/ubuntu/.deno/bin/deno run --allow-all --unstable-kv /home/ubuntu/my-deno-app/app.ts
+/home/ubuntu/.deno/bin/deno run --allow-net --allow-read --allow-write /home/ubuntu/my-deno-app/app.ts
```
-**Give Execution Permission to the Script:**
+Give permission to the script:
-```bash
+```console
chmod +x run-server.sh
```
-**Create Service Files:**
+Create service files:
-```bash
+```console
sudo touch /etc/systemd/system/my-deno-app.service
sudo nano /etc/systemd/system/my-deno-app.service
```
-**In "my-deno-app.service" (change "Description", "WorkingDirectory", and
-"ExecStart" to yours):**
+In "my-deno-app".service (change the "Description", "WorkingDirectory" and
+"ExecStart" to yours):
-```ini
+```
[Unit]
Description=My Deno App
[Service]
-Type=simple
-User=ubuntu
WorkingDirectory=/home/ubuntu/my-deno-app
ExecStart=/home/ubuntu/my-deno-app/run-server.sh
TimeoutSec=30
@@ -895,120 +622,94 @@ RestartSec=1
WantedBy=multi-user.target
```
-**If Your Application Depends on Another Service (e.g., MongoDB):**
+If your application needs to wait for another service to start, such as the
+mongodb database, you can use the ยด[Unit]ยด section like this:
-```ini
+```
[Unit]
Description=My Deno App
After=mongod.service
```
-**Enable the "my-deno-app" Service:**
+Enable the "my-deno-app" service:
-```bash
+```console
sudo systemctl enable my-deno-app.service
```
-**Start and Stop the "my-deno-app" Service:**
+To start and stop the "my-deno-app" service:
-```bash
+```console
sudo service my-deno-app stop
sudo service my-deno-app start
```
-**View Logs:**
+See log:
-```bash
+```console
journalctl -u my-deno-app.service --since=today -e
```
----
-
-### ๐ **Configure HTTPS**
+### Configure HTTPS
-**Install Certbot:**
+Install certbot:
-```bash
+```console
sudo apt install certbot
```
-**Generate Certificates (Port 80 Must Be Free):**
+Generate certificates:
-```bash
-sudo certbot certonly --standalone
+```console
+sudo certbot certonly --manual
```
-**During Setup:**
+In your application, to verify the domain you will need something like:
-When prompted:
-
-```
-Please enter the domain name(s) you would like on your certificate (comma and/or space separated) (Enter 'c' to cancel):
+```typescript
+import { Server, serveStatic } from "https://deno.land/x/faster/mod.ts";
+const server = new Server();
+server.get( //verify http:///.well-known/acme-challenge/
+ "/.well-known/*",
+ serveStatic("./.well-known"), // ex: create .well-known folder in yor app folder
+);
+await server.listen({ port: 80 });
```
-Enter your domains and subdomains, e.g.: `yourdomain.link www.yourdomain.link`
-
-**Run Your Application on HTTPS (Change "yourdomain.link" to Your Domain):**
+To run your application on https (Change "yourdomain.link" to your domain):
```typescript
await server.listen({
port: 443,
- cert: await Deno.readTextFile(
- "/etc/letsencrypt/live/yourdomain.link/fullchain.pem",
- ),
- key: await Deno.readTextFile(
- "/etc/letsencrypt/live/yourdomain.link/privkey.pem",
- ),
+ certFile: "/etc/letsencrypt/live/yourdomain.link/fullchain.pem",
+ keyFile: "/etc/letsencrypt/live/yourdomain.link/privkey.pem",
});
-
-//or with the portable command "serve":
-//in this case you need to pass arguments such as port and certificate in the command.
-export default { fetch: server.fetch };
```
-**Set Up Automatic Certificate Renewal:**
-
-The certificate is valid for a short period. Set up a cron job to renew
-automatically.
+The certificate is valid for a short period. Set crontab to update
+automatically. The command 'sudo crontab' opens roots crontab, all commands are
+executed as sudo. Do like this:
-**Edit Root's Crontab:**
-
-```bash
+```console
sudo crontab -e
```
-**Add to the End of the File (to Check and Renew Every 12 Hours):**
+Add to the end of the file (to check and renew if necessary every 12 hours):
```
0 */12 * * * certbot -q renew --standalone --preferred-challenges=http
```
-**Alternatively, Check Every 7 Days:**
+Or also to check every 7 days:
```
0 0 * * 0 certbot -q renew --standalone --preferred-challenges=http
```
----
-
-## ๐ก **See Also: Faster with React**
-
-Check out the complete framework with Faster and React:
-
-๐
-[https://github.com/hviana/faster_react](https://github.com/hviana/faster_react)
-
----
-
-## ๐จโ๐ป **About**
-
-**Author:** Henrique Emanoel Viana, a Brazilian computer scientist and web
-technology enthusiast.
-
-- ๐ **Phone:** +55 (41) 99999-4664
-- ๐ **Website:**
- [https://sites.google.com/view/henriqueviana](https://sites.google.com/view/henriqueviana)
+## About
-> **Improvements and suggestions are welcome!**
+Author: Henrique Emanoel Viana, a Brazilian computer scientist, enthusiast of
+web technologies, cel: +55 (41) 99999-4664. URL:
+https://sites.google.com/site/henriqueemanoelviana
----
+Improvements and suggestions are welcome!
diff --git a/deno.json b/deno.json
deleted file mode 100644
index aee479b..0000000
--- a/deno.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "@hviana/faster",
- "version": "1.1.2",
- "exports": {
- ".": "./mod.ts",
- "./jose": "./vendor/jose/index.ts",
- "./deno-kv-fs": "./vendor/deno_kv_fs/index.ts"
- }
-}
diff --git a/deps.ts b/deps.ts
index 0052891..120de13 100644
--- a/deps.ts
+++ b/deps.ts
@@ -1,31 +1,26 @@
-export { join } from "jsr:@std/path@^1.0.9";
-
-export {
- DenoKvFs,
- type DirList,
- type File,
- type FileStatus,
- type ReadOptions,
- type SaveOptions,
-} from "jsr:@hviana/deno-kv-fs@^1.0.2";
-import * as deno_kv_fs from "jsr:@hviana/deno-kv-fs@^1.0.2";
-export { deno_kv_fs };
+export { join, SEP } from "https://deno.land/std@0.142.0/path/mod.ts";
export {
ensureDir,
ensureDirSync,
- ensureFile,
ensureFileSync,
move,
-} from "jsr:@std/fs@^1.0.19";
-export { crypto } from "jsr:@std/crypto@^1.0.5";
-export { toReadableStream, toWritableStream } from "jsr:@std/io@^0.225.2";
+} from "https://deno.land/std@0.142.0/fs/mod.ts";
+export { crypto } from "https://deno.land/std@0.142.0/crypto/mod.ts";
+export { MultipartReader } from "https://deno.land/std@0.142.0/mime/multipart.ts";
+export { readableStreamFromReader } from "https://deno.land/std@0.142.0/io/mod.ts";
+export { readerFromStreamReader } from "https://deno.land/std@0.142.0/streams/conversion.ts";
+export {
+ generateSecret,
+ jwtVerify,
+ SignJWT,
+} from "https://deno.land/x/jose@v4.8.1/index.ts";
+
+export { storage } from "https://deno.land/x/fast_storage/mod.ts";
-export type { Cookie } from "jsr:@std/http@^1.0.20/cookie";
+export type { Cookie } from "https://deno.land/std@0.142.0/http/cookie.ts";
export {
deleteCookie,
getCookies,
- getSetCookies,
setCookie,
-} from "jsr:@std/http@^1.0.20/cookie";
-export * as jose from "jsr:@panva/jose@^6.0.12";
+} from "https://deno.land/std@0.142.0/http/cookie.ts";
diff --git a/middlewares/logger.ts b/middlewares/logger.ts
index b24faad..df27617 100644
--- a/middlewares/logger.ts
+++ b/middlewares/logger.ts
@@ -1,67 +1,26 @@
/*
Created by: Henrique Emanoel Viana
Githu: https://github.com/hviana
-Page: https://sites.google.com/view/henriqueviana
+Page: https://sites.google.com/site/henriqueemanoelviana
cel: +55 (41) 99999-4664
*/
-import { Context, NextFunc, RouteFn, Server } from "../server.ts";
-import { DenoKvFs } from "../deps.ts";
-
-export class FasterLog {
- static async write(data: string) {
- const date = Date.now();
- const id = crypto.randomUUID();
- await Server.kv!.set(["faster", "log", date, id], data);
- }
- static async get(
- startMillis: number = 0,
- endMillis: number = Date.now(),
- ): Promise {
- const res: any[] = [];
- const listParamsSaving = [{
- start: ["faster", "log", startMillis],
- end: ["faster", "log", endMillis],
- }, {
- limit: 1000,
- }];
- for await (
- const f of DenoKvFs.pagedListIterator(listParamsSaving, Server.kv!)
- ) {
- res.push({ time: f.key[f.key.length - 2], log: f.value });
- }
- return res;
- }
- static async delete(
- startMillis: number = 0,
- endMillis: number = Date.now(),
- ): Promise {
- const listParamsSaving = [{
- start: ["faster", "log", startMillis],
- end: ["faster", "log", endMillis],
- }, {
- limit: 1000,
- }];
- for await (
- const f of DenoKvFs.pagedListIterator(listParamsSaving, Server.kv!)
- ) {
- await Server.kv!.delete(f.key);
- }
- }
-}
-
-export function logger(salve: boolean = true, print: boolean = true): RouteFn {
+import { Context, NextFunc } from "../server.ts";
+import { ensureFileSync } from "../deps.ts";
+export function logger(
+ file: string = "./log.txt",
+) {
+ ensureFileSync(file);
+ const denoFile = Deno.openSync(file, { write: true, append: true });
return async function (ctx: Context, next: NextFunc) {
- const entry = `${ctx.req.method} ${ctx.url.toString()} ${ctx.req.headers
- .get("Host")!} ${new Date().toISOString()}`;
- if (print) {
- console.log(entry);
- }
- if (salve) {
- await FasterLog.write(
- entry,
- );
- }
+ await Deno.write(
+ denoFile.rid,
+ new TextEncoder().encode(
+ `${ctx.req.method} ${ctx.url.toString()} ${
+ JSON.stringify(ctx.conn.remoteAddr)
+ } ${new Date().toISOString()}\n`,
+ ),
+ );
await next();
};
}
diff --git a/middlewares/parser.ts b/middlewares/parser.ts
index 7faf0b2..c910e42 100644
--- a/middlewares/parser.ts
+++ b/middlewares/parser.ts
@@ -1,11 +1,11 @@
/*
Created by: Henrique Emanoel Viana
Githu: https://github.com/hviana
-Page: https://sites.google.com/view/henriqueviana
+Page: https://sites.google.com/site/henriqueemanoelviana
cel: +55 (41) 99999-4664
*/
-import { Context, NextFunc, ProcessorFunc, RouteFn } from "../server.ts";
+import { Context, NextFunc, ProcessorFunc } from "../server.ts";
const reqParsers: { [key: string]: ProcessorFunc } = {};
const resParsers: { [key: string]: ProcessorFunc } = {
"json": (ctx: Context) => {
@@ -22,7 +22,7 @@ const resParsers: { [key: string]: ProcessorFunc } = {
},
};
-export function res(type: string): RouteFn {
+export function res(type: string) {
return async (ctx: Context, next: NextFunc) => {
if (!resParsers[type]) {
throw new Error(`Response body parser: '${type}' not supported.`);
@@ -31,7 +31,7 @@ export function res(type: string): RouteFn {
await next();
};
}
-export function req(type: string): RouteFn {
+export function req(type: string) {
return async (ctx: Context, next: NextFunc) => {
if (ctx.req.bodyUsed) {
return;
diff --git a/middlewares/proxy.ts b/middlewares/proxy.ts
index c467601..819a982 100644
--- a/middlewares/proxy.ts
+++ b/middlewares/proxy.ts
@@ -1,14 +1,14 @@
/*
Created by: Henrique Emanoel Viana
Githu: https://github.com/hviana
-Page: https://sites.google.com/view/henriqueviana
+Page: https://sites.google.com/site/henriqueemanoelviana
cel: +55 (41) 99999-4664
*/
-import { Context, NextFunc, RouteFn } from "../server.ts";
+import { Context, NextFunc } from "../server.ts";
interface ProxyOptions {
- url: string | ((ctx: Context) => string | Promise);
+ url: string;
replaceReqAndRes?: boolean;
replaceProxyPath?: boolean;
condition?: (context: Context) => Promise | boolean;
@@ -23,18 +23,12 @@ const defaultProxyOptions: ProxyOptions = {
export function proxy(
options: ProxyOptions = defaultProxyOptions,
-): RouteFn {
+) {
const mergedOptions = { ...defaultProxyOptions, ...options };
return async (ctx: Context, next: NextFunc) => {
if (await mergedOptions.condition!(ctx)) {
- let url = "";
- if (typeof mergedOptions.url === "function") {
- url = await mergedOptions.url(ctx);
- } else {
- url = mergedOptions.url;
- }
ctx.extra.proxied = true;
- const proxyURL = new URL(url);
+ const proxyURL = new URL(mergedOptions.url);
if (mergedOptions.replaceProxyPath) {
proxyURL.pathname = ctx.url.pathname;
proxyURL.search = ctx.url.search;
diff --git a/middlewares/rate_limit.ts b/middlewares/rate_limit.ts
index 7d3ed67..763a8a0 100644
--- a/middlewares/rate_limit.ts
+++ b/middlewares/rate_limit.ts
@@ -1,11 +1,11 @@
/*
Created by: Henrique Emanoel Viana
Githu: https://github.com/hviana
-Page: https://sites.google.com/view/henriqueviana
+Page: https://sites.google.com/site/henriqueemanoelviana
cel: +55 (41) 99999-4664
*/
-import { Context, NextFunc, RouteFn, Server } from "../server.ts";
+import { Context, NextFunc } from "../server.ts";
interface RateLimitOptions {
attempts?: number;
@@ -18,7 +18,7 @@ const defaultRateLimitOptions: RateLimitOptions = {
attempts: 30,
interval: 10,
maxTableSize: 100000, //be careful, table uses a lot of memory
- id: (ctx: Context) => Server.getClientIp(ctx.req),
+ id: (ctx: Context) => JSON.stringify(ctx.conn.remoteAddr),
};
function clearMap(
@@ -37,7 +37,7 @@ function clearMap(
}
export function rateLimit(
options: RateLimitOptions = defaultRateLimitOptions,
-): RouteFn {
+) {
const mergedOptions = { ...defaultRateLimitOptions, ...options };
const {
attempts,
@@ -64,7 +64,7 @@ export function rateLimit(
data.attempts++;
}
await next();
- } catch (e: any) {
+ } catch (e) {
ctx.res.headers.set("Content-Type", "application/json");
ctx.res.status = 429;
ctx.res.body = JSON.stringify({
diff --git a/middlewares/redirect.ts b/middlewares/redirect.ts
index a8de69f..54c4bc8 100644
--- a/middlewares/redirect.ts
+++ b/middlewares/redirect.ts
@@ -1,14 +1,14 @@
/*
Created by: Henrique Emanoel Viana
Githu: https://github.com/hviana
-Page: https://sites.google.com/view/henriqueviana
+Page: https://sites.google.com/site/henriqueemanoelviana
cel: +55 (41) 99999-4664
*/
-import { Context, NextFunc, RouteFn } from "../server.ts";
-export function redirect(...params: any[]): RouteFn {
+import { Context, NextFunc } from "../server.ts";
+export function redirect(url: string) {
return async (ctx: Context, next: NextFunc) => {
- ctx.redirect(...params);
+ ctx.redirect(url);
await next();
};
}
diff --git a/middlewares/serve_static.ts b/middlewares/serve_static.ts
index f3fb149..3de1b6c 100644
--- a/middlewares/serve_static.ts
+++ b/middlewares/serve_static.ts
@@ -1,20 +1,20 @@
/*
Created by: Henrique Emanoel Viana
Githu: https://github.com/hviana
-Page: https://sites.google.com/view/henriqueviana
+Page: https://sites.google.com/site/henriqueemanoelviana
cel: +55 (41) 99999-4664
*/
-import { Context, NextFunc, RouteFn } from "../server.ts";
-import { join, toReadableStream } from "../deps.ts";
-export function serveStatic(root: string): RouteFn {
+import { Context, NextFunc } from "../server.ts";
+import { join, readableStreamFromReader } from "../deps.ts";
+export function serveStatic(root: string) {
return async (ctx: Context, next: NextFunc) => {
try {
const file = await Deno.open(
- join(root, ctx.params.wild),
+ join(root, ctx.params[Object.keys(ctx.params)[0]]),
{ read: true },
);
- ctx.res.body = toReadableStream(file);
+ ctx.res.body = readableStreamFromReader(file);
await next();
} catch (e) {
ctx.res.status = 404;
diff --git a/middlewares/session.ts b/middlewares/session.ts
index 4474aca..23b7798 100644
--- a/middlewares/session.ts
+++ b/middlewares/session.ts
@@ -1,122 +1,105 @@
/*
Created by: Henrique Emanoel Viana
Githu: https://github.com/hviana
-Page: https://sites.google.com/view/henriqueviana
+Page: https://sites.google.com/site/henriqueemanoelviana
cel: +55 (41) 99999-4664
*/
-import { Context, NextFunc, RouteFn, Server } from "../server.ts";
-import { crypto, deleteCookie, getCookies, setCookie } from "../deps.ts";
+import { Context, NextFunc, Params, ProcessorFunc } from "../server.ts";
+import {
+ crypto,
+ deleteCookie,
+ getCookies,
+ setCookie,
+ storage,
+} from "../deps.ts";
export type Session = {
key: string;
- value: any;
- created?: number;
+ value: Params;
+ lastAccessTime: number;
};
export class SessionStorageEngine {
- slidingExpiration: number = 0;
- absoluteExpiration: number = 0;
- initialized: boolean = false;
- constructor(
- slidingExpiration: number = 0,
- absoluteExpiration: number = 0,
- ) {
- if (absoluteExpiration > 0 && slidingExpiration > 0) {
- if (absoluteExpiration < slidingExpiration) {
- throw new Error(
- "Absolute Expiration cannot be less than Sliding Expiration.",
- );
- }
- }
- this.slidingExpiration = slidingExpiration;
- this.absoluteExpiration = absoluteExpiration;
+ expiresInMinutes: number;
+ checkInterval: number = 0; //checkInterval may not be necessary depending on the engine
+ constructor(expiresInMinutes: number) {
+ this.expiresInMinutes = expiresInMinutes;
}
async init(): Promise {
throw new Error(`"init" not implemented in: ${this.constructor.name}`);
}
+ async delete(key: string): Promise {
+ throw new Error(`"delete" not implemented in: ${this.constructor.name}`);
+ }
async set(session: Session): Promise {
throw new Error(`"set" not implemented in: ${this.constructor.name}`);
}
async get(key: string): Promise {
throw new Error(`"get" not implemented in: ${this.constructor.name}`);
}
+ async getAll(): Promise {
+ throw new Error(`"getAll" not implemented in: ${this.constructor.name}`);
+ }
}
-export class KVStorageEngine extends SessionStorageEngine {
- constructor(
- slidingExpiration: number = 60,
- absoluteExpiration: number = 0,
- ) {
- super(slidingExpiration, absoluteExpiration);
+export class SQLiteStorageEngine extends SessionStorageEngine {
+ constructor(expiresInMinutes: number) {
+ super(expiresInMinutes);
}
- override async init() {
- }
- override async set(session: Session): Promise {
- const key = ["faster_sessions", session.key];
- var newEntry = false;
- if (session.created == undefined) {
- session.created = Date.now();
- newEntry = true;
- } else {
- if (this.slidingExpiration > 0 && this.absoluteExpiration > 0) {
- if (
- ((Date.now() - session.created) / 1000 / 60) >=
- this.absoluteExpiration
- ) {
- await Server.kv.delete(key);
+ async init(): Promise {
+ const expiresInMS = this.expiresInMinutes * 60 * 1000;
+ clearInterval(this.checkInterval);
+ this.checkInterval = setInterval(async () => {
+ const sessions = await this.getAll();
+ const currentTime = Date.now();
+ for (const s of sessions) {
+ if ((currentTime - s.lastAccessTime) > expiresInMS) {
+ await this.delete(s.key);
}
}
- }
- if (this.slidingExpiration > 0) {
- await Server.kv.set(key, session, {
- expireIn: this.slidingExpiration * 1000 * 60,
- });
- } else if (this.absoluteExpiration > 0 && newEntry) {
- await Server.kv.set(key, session, {
- expireIn: this.absoluteExpiration * 1000 * 60,
- });
- } else {
- await Server.kv.set(key, session);
- }
+ }, expiresInMS);
}
- override async get(key: string): Promise {
- const session: Session = (await Server.kv.get(["faster_sessions", key]))
- .value as Session;
- if (this.slidingExpiration > 0) {
- if (session) {
- await this.set(session);
- }
- }
- return session;
+ async delete(key: string): Promise {
+ await storage.delete(`faster_sessions.${key}`);
+ }
+ async set(session: Session): Promise {
+ return await storage.set(`faster_sessions.${session.key}`, session);
+ }
+ async get(key: string): Promise {
+ return await storage.get(`faster_sessions.${key}`);
+ }
+ async getAll(): Promise {
+ return await storage.getList("faster_sessions.");
}
}
export function session(
- engine: SessionStorageEngine = new KVStorageEngine(60),
-): RouteFn {
+ engine: SessionStorageEngine = new SQLiteStorageEngine(60),
+) {
+ engine.init(); //no await, beware
return async (ctx: Context, next: NextFunc) => {
- if (!engine.initialized) {
- await engine.init();
- }
var key = getCookies(ctx.req.headers).faster_session_id;
- ctx.extra.session = { value: {} };
+ ctx.extra.session = {};
var hasSession = false;
if (key) {
const session_data = await engine.get(key);
if (session_data) {
hasSession = true;
- ctx.extra.session = session_data;
+ ctx.extra.session = session_data.value;
}
}
ctx.postProcessors.add(async (ctx: Context) => {
- if ((Object.keys(ctx.extra.session.value).length > 0) || hasSession) {
+ if ((Object.keys(ctx.extra.session).length > 0) || hasSession) {
if (!key) {
key = crypto.randomUUID();
setCookie(ctx.res.headers, { name: "faster_session_id", value: key });
}
- ctx.extra.session["key"] = key;
- await engine.set(ctx.extra.session);
+ await engine.set({
+ key: key,
+ value: ctx.extra.session,
+ lastAccessTime: Date.now(),
+ });
} else {
if (key) {
deleteCookie(ctx.res.headers, "faster_session_id");
diff --git a/middlewares/set_cors.ts b/middlewares/set_cors.ts
index 149e535..b509445 100644
--- a/middlewares/set_cors.ts
+++ b/middlewares/set_cors.ts
@@ -1,14 +1,14 @@
/*
Created by: Henrique Emanoel Viana
Githu: https://github.com/hviana
-Page: https://sites.google.com/view/henriqueviana
+Page: https://sites.google.com/site/henriqueemanoelviana
cel: +55 (41) 99999-4664
*/
-import { Context, NextFunc, RouteFn } from "../server.ts";
+import { Context, NextFunc } from "../server.ts";
//server.options("/path",setCORS()); // necessary enable pre-fligh request on "/path"
-export function setCORS(origin: string = "*"): RouteFn {
+export function setCORS(origin: string = "*") {
return async (ctx: Context, next: NextFunc) => {
ctx.res.headers.set(
"Access-Control-Expose-Headers",
@@ -18,7 +18,7 @@ export function setCORS(origin: string = "*"): RouteFn {
ctx.res.headers.set("Access-Control-Allow-Origin", origin);
ctx.res.headers.set(
"Access-Control-Allow-Methods",
- "GET,PUT,POST,DELETE,PATCH,HEAD,OPTIONS",
+ "GET,PUT,POST,DELETE,PATCH,OPTIONS",
);
ctx.res.headers.set("Access-Control-Allow-Headers", "*, Authorization");
await next();
diff --git a/middlewares/token.ts b/middlewares/token.ts
index 1cabb5e..186d3df 100644
--- a/middlewares/token.ts
+++ b/middlewares/token.ts
@@ -1,31 +1,31 @@
/*
Created by: Henrique Emanoel Viana
Githu: https://github.com/hviana
-Page: https://sites.google.com/view/henriqueviana
+Page: https://sites.google.com/site/henriqueemanoelviana
cel: +55 (41) 99999-4664
*/
import { Context, NextFunc } from "../server.ts";
-import { jose } from "../deps.ts";
-const randomKey = await jose.generateSecret("HS256");
+import { generateSecret, jwtVerify, SignJWT } from "../deps.ts";
+const randomKey = await generateSecret("HS256");
export class Token {
- static _configs: any = {
+ static #configs: any = {
key: randomKey,
issuer: "urn:faster:issuer",
audience: "urn:faster:audience",
alg: "HS256",
oneHour: "1h",
};
- static setConfigs(configs: any): void {
- Token._configs = { ...Token._configs, ...configs };
+ static setConfigs(configs: any) {
+ Token.#configs = { ...Token.#configs, ...configs };
}
- static setSecret(secret: string): void {
- Token._configs.key = (new TextEncoder()).encode(secret);
+ static setSecret(secret: string) {
+ Token.#configs.key = (new TextEncoder()).encode(secret);
}
- static async getPayload(token: string): Promise {
- const { payload, protectedHeader } = await jose.jwtVerify(
+ static async getPayload(token: string) {
+ const { payload, protectedHeader } = await jwtVerify(
token,
- Token._configs.key,
+ Token.#configs.key,
);
return payload;
}
@@ -39,7 +39,7 @@ export class Token {
ctx.extra.tokenPayload = await Token.getPayload(token);
ctx.extra.token = token;
await next();
- } catch (e: any) {
+ } catch (e) {
ctx.res.headers.set("Content-Type", "application/json");
ctx.res.status = 403;
ctx.res.body = JSON.stringify({
@@ -47,18 +47,15 @@ export class Token {
});
}
}
- static async generate(
- data: any = {},
- exp = Token._configs.oneHour,
- ): Promise {
- const jwt = await new jose.SignJWT(data)
- .setProtectedHeader({ alg: Token._configs.alg })
+ static async generate(data: any = {}, exp = Token.#configs.oneHour) {
+ const jwt = await new SignJWT(data)
+ .setProtectedHeader({ alg: Token.#configs.alg })
.setIssuedAt()
- .setIssuer(Token._configs.issuer)
- .setAudience(Token._configs.audience);
+ .setIssuer(Token.#configs.issuer)
+ .setAudience(Token.#configs.audience);
if (exp) {
jwt.setExpirationTime(exp);
}
- return jwt.sign(Token._configs.key);
+ return jwt.sign(Token.#configs.key);
}
}
diff --git a/middlewares/upload.ts b/middlewares/upload.ts
index 31d6b8e..90cbcc5 100644
--- a/middlewares/upload.ts
+++ b/middlewares/upload.ts
@@ -1,160 +1,208 @@
/*
Created by: Henrique Emanoel Viana
Githu: https://github.com/hviana
-Page: https://sites.google.com/view/henriqueviana
+Page: https://sites.google.com/site/henriqueemanoelviana
cel: +55 (41) 99999-4664
*/
-import { Context, NextFunc, RouteFn, Server } from "../server.ts";
-
+import {
+ crypto,
+ ensureDir,
+ ensureDirSync,
+ join,
+ move,
+ MultipartReader,
+ readerFromStreamReader,
+ SEP,
+} from "../deps.ts";
+import { Context, NextFunc } from "../server.ts";
interface UploadOptions {
- allowedExtensions?: (ctx: Context) => Promise> | Array;
- maxSizeBytes?: (ctx: Context) => Promise | number;
- maxFileSizeBytes?: (ctx: Context) => Promise | number;
- chunksPerSecond?: (ctx: Context) => Promise | number;
- maxClientIdConcurrentReqs?: (ctx: Context) => Promise | number;
- clientId?: (
- ctx: Context,
- ) => Promise | string | number | undefined;
- validateAccess?: (ctx: Context, path: string[]) => Promise | boolean;
-}
-interface DownloadOptions {
- chunksPerSecond?: (ctx: Context) => Promise | number;
- clientId?: (
- ctx: Context,
- ) => Promise | string | number | undefined;
- validateAccess?: (ctx: Context, path: string[]) => Promise | boolean;
- maxClientIdConcurrentReqs?: (ctx: Context) => Promise | number;
- maxDirEntriesPerSecond?: (ctx: Context) => Promise | number;
- pagination?: (ctx: Context) => Promise | boolean;
- cursor?: (ctx: Context) => Promise | string | undefined; //for readDir, If there is a next page.
+ path?: string;
+ extensions?: Array;
+ maxSizeBytes?: number;
+ maxFileSizeBytes?: number;
+ saveFile?: boolean;
+ readFile?: boolean;
+ useCurrentDir?: boolean;
}
+
const defaultUploadOptions: UploadOptions = {
- allowedExtensions: (ctx: Context) => [],
- maxSizeBytes: (ctx: Context) => Number.MAX_SAFE_INTEGER,
- maxFileSizeBytes: (ctx: Context) => Number.MAX_SAFE_INTEGER,
- chunksPerSecond: (ctx: Context) => Number.MAX_SAFE_INTEGER,
- maxClientIdConcurrentReqs: (ctx: Context) => Number.MAX_SAFE_INTEGER,
- clientId: (ctx: Context) => undefined,
- validateAccess: (ctx: Context, path: string[]) => true,
-};
-const defaultDownloadOptions: DownloadOptions = {
- chunksPerSecond: (ctx: Context) => Number.MAX_SAFE_INTEGER,
- clientId: (ctx: Context) => undefined,
- validateAccess: (ctx: Context, path: string[]) => true,
- maxClientIdConcurrentReqs: (ctx: Context) => Number.MAX_SAFE_INTEGER,
- maxDirEntriesPerSecond: (ctx: Context) => Number.MAX_SAFE_INTEGER,
- pagination: (ctx: Context) => false,
- cursor: (ctx: Context) => undefined,
+ path: "uploads",
+ extensions: [],
+ maxSizeBytes: Number.MAX_SAFE_INTEGER,
+ maxFileSizeBytes: Number.MAX_SAFE_INTEGER,
+ saveFile: true,
+ readFile: false,
+ useCurrentDir: true,
};
-function upload(
+const enc = new TextEncoder();
+
+export function upload(
options: UploadOptions = defaultUploadOptions,
-): RouteFn {
+) {
const mergedOptions = { ...defaultUploadOptions, ...options };
const {
- allowedExtensions,
+ path,
+ extensions,
maxSizeBytes,
maxFileSizeBytes,
- chunksPerSecond,
- maxClientIdConcurrentReqs,
- clientId,
- validateAccess,
+ saveFile,
+ readFile,
+ useCurrentDir,
} = mergedOptions;
+ ensureDirSync(join(Deno.cwd(), "temp_uploads"));
return async (ctx: Context, next: NextFunc) => {
- const reqBody = await ctx.req.formData();
- const existingFileNamesInTheUpload: { [key: string]: number } = {};
- const res: any = {};
if (
- parseInt(ctx.req.headers.get("content-length")!) >
- await maxSizeBytes!(ctx)
+ parseInt(ctx.req.headers.get("content-length")!) > maxSizeBytes!
) {
throw new Error(
`Maximum total upload size exceeded, size: ${
ctx.req.headers.get("content-length")
- } bytes, maximum: ${await maxSizeBytes!(ctx)} bytes. `,
+ } bytes, maximum: ${maxSizeBytes} bytes. `,
);
}
- for (const item of reqBody.entries()) {
- if (item[1] instanceof File) {
- const formField: any = item[0];
- const fileData: any = item[1];
- if (!existingFileNamesInTheUpload[fileData.name]) {
- existingFileNamesInTheUpload[fileData.name] = 1;
- } else {
- existingFileNamesInTheUpload[fileData.name]++;
- }
- let prepend = "";
- if (existingFileNamesInTheUpload[fileData.name] > 1) {
- prepend += existingFileNamesInTheUpload[fileData.name].toString();
- }
- var sep = "";
- if (!ctx.params.wild.endsWith("/")) {
- sep = "/";
+ const boundaryRegex = /^multipart\/form-data;\sboundary=(?.*)$/;
+ let match: RegExpMatchArray | null;
+ if (
+ ctx.req.headers.get("content-type") &&
+ (match = ctx.req.headers.get("content-type")!.match(
+ boundaryRegex,
+ ))
+ ) {
+ const formBoundary: string = match.groups!.boundary;
+ const mr = new MultipartReader(
+ readerFromStreamReader(ctx.req.body!.getReader()),
+ formBoundary,
+ );
+ const form = await mr.readForm(0);
+ const res: any = {};
+ const entries: any = Array.from(form.entries());
+ let validations = "";
+ for (const item of entries) {
+ const values: any = [].concat(item[1]);
+ for (const val of values) {
+ if (val.filename !== undefined) {
+ if (extensions!.length > 0) {
+ const ext = val.filename.split(".").pop();
+ if (!extensions!.includes(ext)) {
+ validations +=
+ `The file extension is not allowed (${ext} in ${val.filename}), allowed extensions: ${extensions}. `;
+ }
+ }
+ if (val.size > maxFileSizeBytes!) {
+ validations +=
+ `Maximum file upload size exceeded, file: ${val.filename}, size: ${val.size} bytes, maximum: ${maxFileSizeBytes} bytes. `;
+ }
+ }
}
- const path = Server.kvFs!.URIComponentToPath(
- ctx.params.wild + sep + prepend + fileData.name,
- );
- let resData = await Server.kvFs!.save({
- path: path,
- content: fileData.stream(),
- allowedExtensions: await allowedExtensions!(ctx),
- maxFileSizeBytes: await maxFileSizeBytes!(ctx),
- chunksPerSecond: await chunksPerSecond!(ctx),
- maxClientIdConcurrentReqs: await maxClientIdConcurrentReqs!(ctx),
- clientId: await clientId!(ctx),
- validateAccess: async (path: string[]) =>
- await validateAccess!(ctx, path),
- });
- if (res[formField] !== undefined) {
- if (Array.isArray(res[formField])) {
- res[formField].push(resData);
- } else {
- res[formField] = [res[formField], resData];
+ }
+ if (validations != "") {
+ await form.removeAll();
+ throw new Error(validations);
+ }
+ for (const item of entries) {
+ const formField: any = item[0];
+ const filesData: any = [].concat(item[1]);
+ for (const fileData of filesData) {
+ if (fileData.tempfile !== undefined) {
+ const resData = fileData;
+ if (readFile) {
+ resData["data"] = await Deno.readFile(resData["tempfile"]);
+ }
+ if (saveFile) {
+ let uploadPath = path;
+ const d = new Date();
+ const uuid = join(
+ d.getFullYear().toString(),
+ (d.getMonth() + 1).toString(),
+ d.getDate().toString(),
+ d.getHours().toString(),
+ d.getMinutes().toString(),
+ d.getSeconds().toString(),
+ crypto.randomUUID(),
+ );
+ uploadPath = join(path!, uuid);
+ let fullPath = uploadPath;
+ if (useCurrentDir) {
+ fullPath = join(Deno.cwd(), fullPath!);
+ }
+ await ensureDir(fullPath!);
+ await move(
+ fileData.tempfile,
+ join(fullPath!, fileData.filename),
+ );
+ delete resData["tempfile"];
+ resData["id"] = uuid.replace(/\\/g, "/");
+ resData["url"] = encodeURI(
+ join(uploadPath!, fileData.filename).replace(/\\/g, "/"),
+ );
+ resData["uri"] = join(fullPath!, fileData.filename);
+ } else {
+ const tempFileName = resData.tempfile.split(SEP).pop();
+ const pathTempFile = join(
+ Deno.cwd(),
+ "temp_uploads",
+ tempFileName,
+ );
+ await move(
+ resData.tempfile,
+ pathTempFile,
+ );
+ resData.tempfile = pathTempFile;
+ }
+ if (res[formField] !== undefined) {
+ if (Array.isArray(res[formField])) {
+ res[formField].push(resData);
+ } else {
+ res[formField] = [res[formField], resData];
+ }
+ } else {
+ res[formField] = resData;
+ }
}
- } else {
- res[formField] = resData;
}
}
+ ctx.extra.uploadedFiles = res;
+ } else {
+ throw new Error(
+ 'Invalid upload data, request must contains a body with form "multipart/form-data", and inputs with type="file". ',
+ );
}
- ctx.extra.uploadedFiles = res;
await next();
};
}
-
-function download(
- options: DownloadOptions = defaultDownloadOptions,
-): RouteFn {
- const mergedOptions = { ...defaultDownloadOptions, ...options };
- const {
- chunksPerSecond,
- clientId,
- validateAccess,
- maxClientIdConcurrentReqs,
- maxDirEntriesPerSecond,
- pagination,
- cursor,
- } = mergedOptions;
+export function preUploadValidate(
+ extensions: Array = [],
+ maxSizeBytes: number = Number.MAX_SAFE_INTEGER,
+ maxFileSizeBytes: number = Number.MAX_SAFE_INTEGER,
+) {
return async (ctx: Context, next: NextFunc) => {
- const path = Server.kvFs!.URIComponentToPath(ctx.params.wild);
- const file = await Server.kvFs!.read({
- path: path,
- chunksPerSecond: await chunksPerSecond!(ctx),
- clientId: await clientId!(ctx),
- validateAccess: async (path: string[]) =>
- await validateAccess!(ctx, path),
- maxClientIdConcurrentReqs: await maxClientIdConcurrentReqs!(ctx),
- maxDirEntriesPerSecond: await maxDirEntriesPerSecond!(ctx),
- pagination: await pagination!(ctx),
- cursor: await cursor!(ctx),
- });
- if (file) {
- ctx.res.body = (file as any).content;
- await next();
- } else {
- ctx.res.status = 404;
+ const jsonData = (await ctx.req.json())["value"];
+ let totalBytes = 0;
+ let validations = "";
+ for (const iName in jsonData) {
+ const files: any = [].concat(jsonData[iName]);
+ for (const file of files) {
+ totalBytes += jsonData[iName].size;
+ if (file.size > maxFileSizeBytes) {
+ validations +=
+ `Maximum file upload size exceeded, file: ${file.name}, size: ${file.size} bytes, maximum: ${maxFileSizeBytes} bytes. `;
+ }
+ if (!extensions.includes(file.name.split(".").pop())) {
+ validations += `The file extension is not allowed (${
+ file.name.split(".").pop()
+ } in ${file.name}), allowed extensions: ${extensions}. `;
+ }
+ }
+ }
+ if (totalBytes > maxSizeBytes) {
+ validations +=
+ `Maximum total upload size exceeded, size: ${totalBytes} bytes, maximum: ${maxSizeBytes} bytes. `;
}
+ if (validations != "") {
+ throw new Error(validations);
+ }
+ await next();
};
}
-
-export { download, upload };
diff --git a/mod.ts b/mod.ts
index 39f3e61..e5592e6 100644
--- a/mod.ts
+++ b/mod.ts
@@ -1,4 +1,4 @@
-export { FasterLog, logger } from "./middlewares/logger.ts";
+export { logger } from "./middlewares/logger.ts";
export { proxy } from "./middlewares/proxy.ts";
export { req, res } from "./middlewares/parser.ts";
export { rateLimit } from "./middlewares/rate_limit.ts";
@@ -6,11 +6,11 @@ export { redirect } from "./middlewares/redirect.ts";
export { serveStatic } from "./middlewares/serve_static.ts";
export { setCORS } from "./middlewares/set_cors.ts";
export { Token } from "./middlewares/token.ts";
-export { download, upload } from "./middlewares/upload.ts";
+export { preUploadValidate, upload } from "./middlewares/upload.ts";
export {
- KVStorageEngine,
session,
SessionStorageEngine,
+ SQLiteStorageEngine,
} from "./middlewares/session.ts";
export type { Session } from "./middlewares/session.ts";
@@ -27,13 +27,4 @@ export type {
export type { Cookie } from "./deps.ts";
-export {
- DenoKvFs,
- type DirList,
- type File,
- type FileStatus,
- type ReadOptions,
- type SaveOptions,
-} from "./deps.ts";
-
-export { deleteCookie, getCookies, getSetCookies, setCookie } from "./deps.ts";
+export { deleteCookie, getCookies, setCookie } from "./deps.ts";
diff --git a/server.ts b/server.ts
index ffbb2f0..1276bc5 100644
--- a/server.ts
+++ b/server.ts
@@ -1,10 +1,10 @@
/*
Created by: Henrique Emanoel Viana
Githu: https://github.com/hviana
-Page: https://sites.google.com/view/henriqueviana
+Page: https://sites.google.com/site/henriqueemanoelviana
cel: +55 (41) 99999-4664
*/
-import { DenoKvFs } from "./deps.ts";
+
export type Params = {
[key: string]: string;
};
@@ -19,6 +19,9 @@ export type ContextResponse = {
};
export class Context {
+ #conn: Deno.Conn;
+ #httpConn: Deno.HttpConn;
+ #requestEvent: Deno.RequestEvent;
#params: Params;
#url: URL;
req: Request;
@@ -27,11 +30,17 @@ export class Context {
error: Error | undefined = undefined;
#postProcessors: Set = new Set();
constructor(
+ conn: Deno.Conn,
+ httpConn: Deno.HttpConn,
+ requestEvent: Deno.RequestEvent,
params: Params,
url: URL,
req: Request,
hasRoute: boolean,
) {
+ this.#conn = conn;
+ this.#httpConn = httpConn;
+ this.#requestEvent = requestEvent;
this.#params = params;
this.#url = url;
this.req = req;
@@ -43,25 +52,31 @@ export class Context {
status: 200,
statusText: "",
};
- get params(): Params {
+ get conn() {
+ return this.#conn;
+ }
+ get httpConn() {
+ return this.#httpConn;
+ }
+ get requestEvent() {
+ return this.#requestEvent;
+ }
+ get params() {
return this.#params;
}
- get url(): URL {
+ get url() {
return this.#url;
}
- get extra(): any {
+ get extra() {
return this.#extra;
}
- get postProcessors(): Set {
+ get postProcessors() {
return this.#postProcessors;
}
- redirect(...params: any[]): void {
+ redirect(url: string) {
this.postProcessors.add((ctx: Context) => {
- if (params.length < 2) {
- params.unshift(302);
- }
- ctx.res.headers.set("Location", params[1]);
- ctx.res.status = params[0];
+ ctx.res.headers.set("Location", url);
+ ctx.res.status = 301;
});
}
}
@@ -74,7 +89,7 @@ export function parse(
loose?: boolean,
): { keys: string[]; pattern: RegExp } {
if (str instanceof RegExp) return { keys: [], pattern: str };
- let c: string,
+ var c: string,
o: number,
tmp: string | undefined,
ext: number,
@@ -86,13 +101,11 @@ export function parse(
while (tmp = arr.shift()) {
c = tmp[0];
if (c === "*") {
- //@ts-ignore
keys.push("wild");
pattern += "/(.*)";
} else if (c === ":") {
o = tmp.indexOf("?", 1);
ext = tmp.indexOf(".", 1);
- //@ts-ignore
keys.push(tmp.substring(1, !!~o ? o : !!~ext ? ext : tmp.length));
pattern += !!~o && !~ext ? "(?:/([^/]+?))?" : "/([^/]+?)";
if (!!~ext) pattern += (!!~o ? "?" : "") + "\\" + tmp.substring(ext);
@@ -133,82 +146,22 @@ export type Route = {
handlers: RouteFn[];
};
export class Server {
- static kv: Deno.Kv;
- static kvFs: DenoKvFs;
- static setKv(kv: Deno.Kv) {
- Server.kv = kv;
- Server.kvFs = new DenoKvFs(Server.kv);
- }
- static getClientIp(
- req: Request,
- info: { remoteAddr: Deno.Addr | Record } = { remoteAddr: {} },
- ): string {
- const fwd = req.headers.get("forwarded");
- if (fwd) {
- const m = fwd.match(/for=(?:(?:"([^"]+)")|\[([^\]]+)\]|([^;,\s]+))/i);
- const ip = m?.[1] || m?.[2] || m?.[3];
- if (ip) return ip;
- }
- const xff = req.headers.get("x-forwarded-for");
- if (xff) {
- const ip = xff.split(",")[0].trim();
- if (ip) return ip;
- }
- const xrip = req.headers.get("x-real-ip");
- if (xrip) return xrip;
-
- const ra = info.remoteAddr as Deno.NetAddr;
- return "hostname" in ra ? ra.hostname : "unknown";
- }
- #ac = new AbortController();
// NOTE: This is transpiled into the constructor, therefore equivalent to this.routes = [];
#routes: Route[] = [];
- //@ts-ignore
- server: Deno.HttpServer;
- openedSockets: Map = new Map();
// NOTE: Using .bind can significantly increase perf compared to arrow functions.
- public all: Function = this.#add.bind(this, "ALL");
- public get: Function = this.#add.bind(this, "GET");
- public head: Function = this.#add.bind(this, "HEAD");
- public patch: Function = this.#add.bind(this, "PATCH");
- public options: Function = this.#add.bind(this, "OPTIONS");
- public connect: Function = this.#add.bind(this, "CONNECT");
- public delete: Function = this.#add.bind(this, "DELETE");
- public trace: Function = this.#add.bind(this, "TRACE");
- public post: Function = this.#add.bind(this, "POST");
- public put: Function = this.#add.bind(this, "PUT");
+ public all = this.#add.bind(this, "ALL");
+ public get = this.#add.bind(this, "GET");
+ public head = this.#add.bind(this, "HEAD");
+ public patch = this.#add.bind(this, "PATCH");
+ public options = this.#add.bind(this, "OPTIONS");
+ public connect = this.#add.bind(this, "CONNECT");
+ public delete = this.#add.bind(this, "DELETE");
+ public trace = this.#add.bind(this, "TRACE");
+ public post = this.#add.bind(this, "POST");
+ public put = this.#add.bind(this, "PUT");
- public resetRoutes() {
- this.#routes = [];
- }
-
- acceptOrRejectSocketConn = async (ctx: Context): Promise => {
- return undefined;
- };
- onSocketMessage = async (
- id: string,
- socket: WebSocket,
- event: any,
- ): Promise => {
- };
- onSocketClosed = async (id: string, socket: WebSocket): Promise => {
- };
- onSocketOpen = async (id: string, socket: WebSocket): Promise => {
- };
- onSocketError = async (id: string, socket: WebSocket): Promise => {
- };
-
- public useAtBeginning(...handlers: RouteFn[]): Server {
- this.#routes.unshift({
- keys: [],
- method: "ALL",
- handlers,
- });
- return this;
- }
-
- public use(...handlers: RouteFn[]): Server {
+ public use(...handlers: RouteFn[]) {
this.#routes.push({
keys: [],
method: "ALL",
@@ -242,155 +195,103 @@ export class Server {
ctx,
async () => await this.#middlewareHandler(fns, fnIndex + 1, ctx),
);
- } catch (e: any) {
+ } catch (e) {
ctx.error = e;
}
}
}
- async serveHandler(
- request: Request,
- ): Promise {
+ async #handleRequest(conn: Deno.Conn) {
try {
- const req = request;
- const url = new URL(request.url);
- const requestHandlers: RouteFn[] = [];
- const params: Params = {};
- const len = this.#routes.length;
- let hasRoute = false;
- for (let i = 0; i < len; i++) {
- const r = this.#routes[i];
- const keyLength = r.keys.length;
- let matches: null | string[] = null;
- if (
- r.pattern === undefined ||
- (req.method === r.method &&
- (matches = r.pattern.exec(
- url.pathname,
- )))
- ) {
- if (r.pattern) {
- hasRoute = true;
- if (keyLength > 0) {
- if (matches) {
- let inc = 0;
- while (inc < keyLength) {
- const prevInc = inc;
- const m = matches[++inc];
- if (m != undefined) {
- params[r.keys[prevInc]] = decodeURIComponent(m);
+ const httpConn = Deno.serveHttp(conn);
+ for await (const requestEvent of httpConn) {
+ const req = requestEvent.request;
+ const url = new URL(requestEvent.request.url);
+ const requestHandlers: RouteFn[] = [];
+ const params: Params = {};
+ const len = this.#routes.length;
+ var hasRoute = false;
+ for (var i = 0; i < len; i++) {
+ const r = this.#routes[i];
+ const keyLength = r.keys.length;
+ var matches: null | string[] = null;
+ if (
+ r.pattern === undefined ||
+ (req.method === r.method &&
+ (matches = r.pattern.exec(
+ url.pathname,
+ )))
+ ) {
+ if (r.pattern) {
+ hasRoute = true;
+ if (keyLength > 0) {
+ if (matches) {
+ var inc = 0;
+ while (inc < keyLength) {
+ params[r.keys[inc]] = decodeURIComponent(matches[++inc]);
}
}
}
}
+ requestHandlers.push(...r.handlers);
}
- requestHandlers.push(...r.handlers);
}
- }
- const ctx = new Context(
- params,
- url,
- req,
- hasRoute,
- );
- if (req.headers.get("upgrade") == "websocket") {
- const connectId = await this.acceptOrRejectSocketConn(ctx);
- if (!connectId) {
- ctx.res.status = 500;
- return new Response(ctx.res.body, {
- status: ctx.res.status,
- });
- }
- const { socket, response } = Deno.upgradeWebSocket(req);
- const existingSocket = this.openedSockets.get(connectId);
- if (existingSocket) {
- existingSocket.close();
- }
- this.openedSockets.set(connectId, socket);
- socket.onmessage = async (event) => {
- try {
- await this.onSocketMessage(connectId, socket, event);
- } catch (e) {
- console.log(e);
- }
- };
- socket.onclose = async () => {
- try {
- await this.onSocketClosed(connectId, socket);
- } catch (e) {
- console.log(e);
- }
- this.openedSockets.delete(connectId);
- };
- socket.onerror = async () => {
- try {
- await this.onSocketError(connectId, socket);
- } catch (e) {
- console.log(e);
- }
- this.openedSockets.delete(connectId);
- };
- socket.onopen = async () => {
+ var ctx = new Context(
+ conn,
+ httpConn,
+ requestEvent,
+ params,
+ url,
+ req,
+ hasRoute,
+ );
+ await this.#middlewareHandler(requestHandlers, 0, ctx);
+ if (!ctx.error) {
try {
- await this.onSocketOpen(connectId, socket);
+ for (const p of ctx.postProcessors) {
+ await p(ctx);
+ }
} catch (e) {
- console.log(e);
+ ctx.error = e;
}
- this.openedSockets.delete(connectId);
- };
- return response;
- }
- await this.#middlewareHandler(requestHandlers, 0, ctx);
- if (!ctx.error) {
- try {
- for (const p of ctx.postProcessors) {
- await p(ctx);
- }
- } catch (e: any) {
- ctx.error = e;
}
- }
- if (ctx.res instanceof Response) {
if (ctx.error) {
- console.log(ctx.error);
+ ctx.res.status = 500;
+ ctx.res.headers.set(
+ "Content-Type",
+ "application/json",
+ );
+ ctx.res.body = JSON.stringify({
+ msg: (ctx.error.message || ctx.error),
+ stack: ctx.error.stack,
+ });
}
- return ctx.res;
- }
- if (ctx.error) {
- ctx.res.status = 500;
- ctx.res.headers.set(
- "Content-Type",
- "application/json",
+ await requestEvent.respondWith(
+ new Response(ctx.res.body, {
+ headers: ctx.res.headers,
+ status: ctx.res.status,
+ statusText: ctx.res.statusText,
+ }),
);
- ctx.res.body = JSON.stringify({
- msg: (ctx.error.message || JSON.stringify(ctx.error)),
- stack: ctx.error.stack,
- });
}
- return new Response(ctx.res.body, {
- headers: ctx.res.headers,
- status: ctx.res.status,
- statusText: ctx.res.statusText,
- });
} catch (e) {
console.log(e);
- return new Response();
}
}
- public async listen(options: any) { //save as Deno.Serve options
- if (this.server) {
- this.#ac.abort();
- await this.server.finished;
+
+ public async listen(serverParams: any) {
+ const server = (serverParams.certFile || serverParams.port === 443)
+ ? Deno.listenTls(serverParams)
+ : Deno.listen(serverParams);
+ try {
+ for await (const conn of server) {
+ this.#handleRequest(conn);
+ }
+ } catch (e) {
+ console.log(e);
+ if (e.name === "NotConnected") {
+ await this.listen(serverParams);
+ }
}
- this.#ac = new AbortController();
- //@ts-ignore
- this.server = Deno.serve(
- { ...options, signal: this.#ac.signal },
- (request: Request, info: Deno.ServeHandlerInfo) =>
- this.serveHandler(request),
- );
}
- fetch = async (_req: Request): Promise => {
- return await this.serveHandler(_req);
- };
}
diff --git a/vendor/deno_kv_fs/index.ts b/vendor/deno_kv_fs/index.ts
deleted file mode 100644
index 0c28427..0000000
--- a/vendor/deno_kv_fs/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export {
- DenoKvFs,
- type DirList,
- type File,
- type FileStatus,
- type ReadOptions,
- type SaveOptions,
-} from "../../deps.ts";
diff --git a/vendor/jose/index.ts b/vendor/jose/index.ts
deleted file mode 100644
index 32d31c9..0000000
--- a/vendor/jose/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { jose } from "../../deps.ts";