Skip to content

Commit

Permalink
fix: download reponse
Browse files Browse the repository at this point in the history
  • Loading branch information
izatop committed Jul 24, 2023
1 parent 8569e54 commit 008e95c
Show file tree
Hide file tree
Showing 10 changed files with 94 additions and 43 deletions.
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@commitlint/cli": "^17.6.7",
"@commitlint/config-conventional": "^17.6.7",
"@types/jest": "^29.5.3",
"@types/node": "^20.4.2",
"@types/node": "^20.4.4",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"cross-env": "^7.0.3",
Expand Down
10 changes: 10 additions & 0 deletions packages/app/src/Request/KeyValueMap.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {isArray} from "@bunt/is";
import {IKeyValueMap} from "../interfaces.js";

export class KeyValueMap implements IKeyValueMap {
Expand Down Expand Up @@ -27,6 +28,15 @@ export class KeyValueMap implements IKeyValueMap {
return this.#map.get(name) || defaultValue || "";
}

public append(input: Record<string, string> | [string, string][]): this {
const entries = isArray(input) ? input : Object.entries(input);
for (const [key, value] of entries) {
this.set(key, value);
}

return this;
}

public toJSON(): {[p: string]: string} {
const object: {[p: string]: string} = {};
for (const [key, value] of this.#map.entries()) {
Expand Down
68 changes: 55 additions & 13 deletions packages/web/src/Transport/Response/DownloadResponse.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,60 @@
import {createReadStream, statSync} from "fs";
import {createReadStream} from "fs";
import {Readable} from "stream";
import {ResponseAbstract} from "./ResponseAbstract.js";

export class DownloadResponse extends ResponseAbstract<string> {
constructor(filename: string, path: string, mimeType: string) {
const {size} = statSync(path);
super(path, {headers: {
"Content-Disposition": `attachment; filename=${filename}`,
"Content-Length": size.toString(),
"Content-Type": mimeType,
}});
import {isString} from "@bunt/is";
import {Defer} from "@bunt/async";
import {ResponseAbstract, ResponseArgs} from "./ResponseAbstract.js";

type DownloadSource = string | Readable;
type DownloadOptions = {
filename: string;
mimeType: string;
source: DownloadSource;
size?: number;
};

export class DownloadResponse extends ResponseAbstract<Readable> {
constructor(options: DownloadOptions) {
super(...createOptions(options));
}

public serialize(source: Readable): Readable {
return source;
}
}

async function createHeaders(readable: Readable, options: DownloadOptions): Promise<Record<string, string>> {
const size = options.size ?? await getReadableLength(readable);
const headers: Record<string, string> = {
"Content-Disposition": `attachment; filename=${options.filename}`,
"Content-Length": size.toString(),
"Content-Type": options.mimeType,
};

return headers;
}

async function getReadableLength(readable: Readable): Promise<number> {
const deferSize = new Defer<number>();
if (readable.readable) {
deferSize.resolve(readable.readableLength);
} else {
readable.on("readable", () => deferSize.resolve(readable.readableLength));
readable.on("error", deferSize.reject);
}

public stringify(path: string): Readable {
return createReadStream(path);
return deferSize;
}

function factoryReadableStream(options: DownloadOptions): Readable {
if (isString(options.source)) {
return createReadStream(options.source);
}

return options.source;
}

function createOptions(options: DownloadOptions): ResponseArgs<Readable> {
const readable = factoryReadableStream(options);

return [readable, {headers: createHeaders(readable, options)}];
}
2 changes: 1 addition & 1 deletion packages/web/src/Transport/Response/JSONResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {ResponseAbstract} from "./ResponseAbstract.js";
export class JSONResponse<T> extends ResponseAbstract<T> {
public readonly type = "application/json";

public stringify(data: T): string {
public serialize(data: T): string {
return JSON.stringify(data);
}
}
2 changes: 1 addition & 1 deletion packages/web/src/Transport/Response/NoContentResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export class NoContentResponse extends ResponseAbstract<undefined> {
super(undefined, {status: "No Content", ...options, code: 204});
}

public stringify(): string {
public serialize(): string {
return "";
}
}
2 changes: 1 addition & 1 deletion packages/web/src/Transport/Response/RedirectResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class RedirectResponse extends ResponseAbstract<undefined> {
super(undefined, {...options, headers, code: code});
}

public stringify(): string {
public serialize(): string {
return "";
}
}
39 changes: 19 additions & 20 deletions packages/web/src/Transport/Response/ResponseAbstract.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import {Readable} from "stream";
import {isFunction, isInstanceOf, isNumber, isString, Promisify} from "@bunt/util";
import * as HTTP from "http-status";
import {StrictKeyValueMap} from "@bunt/app";
import {Headers} from "../Headers.js";
import {Cookie, CookieOptions} from "./Cookie.js";

export interface IResponseOptions {
code?: number;
status?: string;
headers?: {[key: string]: string} | Headers;
headers?: Promisify<Record<string, string> | Headers>;
}

export type ResponseArgs<T> = [
reponse: Promisify<T> | (() => Promisify<T>),
options?: IResponseOptions
];

export interface IResponseAnswer {
code: number;
status?: string;
body: string | Buffer | Readable;
headers: {[key: string]: string};
headers: Record<string, string>;
cookies: Cookie[];
}

Expand All @@ -25,18 +31,17 @@ export abstract class ResponseAbstract<T> {
public readonly encoding: string = "utf-8";

readonly #cookies = new Map<string, Cookie>();
readonly #headers: {[key: string]: string};
#data: Promisify<T>;
readonly #headers: Promisify<{[key: string]: string}>;
#response: Promisify<T>;

constructor(data: Promisify<T> | (() => Promisify<T>), options: IResponseOptions = {}) {
this.#data = isFunction(data) ? data() : data;
constructor(...[response, options = {}]: ResponseArgs<T>) {
this.#response = isFunction(response) ? response() : response;

const {code, status, headers} = options;
if (isNumber(code) && code > 0) {
this.code = code;
}


this.status = status;
if (!this.status) {
const suggest = HTTP[this.code];
Expand All @@ -46,7 +51,7 @@ export abstract class ResponseAbstract<T> {
if (isInstanceOf(headers, Headers)) {
this.#headers = headers.toJSON();
} else {
this.#headers = headers || {};
this.#headers = headers || Promise.resolve({});
}
}

Expand All @@ -55,7 +60,7 @@ export abstract class ResponseAbstract<T> {
}

public setContent(data: Promisify<T>): void {
this.#data = data;
this.#response = data;
}

public setCookie(name: string, value: string, options: CookieOptions): void {
Expand All @@ -66,29 +71,23 @@ export abstract class ResponseAbstract<T> {
return this.#cookies.has(name);
}

public getHeaders(): Record<any, string> {
return {
...this.#headers,
"content-type": this.getContentType(),
};
}

public async getResponse(): Promise<IResponseAnswer> {
const {status, code, cookies} = this;
const headers = this.getHeaders();
const headersMap = new StrictKeyValueMap([["content-type", this.getContentType()]]);
headersMap.append(await this.#headers);

return {
code,
status,
headers,
cookies,
body: this.stringify(await this.#data),
headers: headersMap.toJSON(),
body: this.serialize(await this.#response),
};
}

public getContentType(): string {
return `${this.type}; charset=${this.encoding}`;
}

protected abstract stringify(data: T): string | Buffer | Readable;
protected abstract serialize(data: T): string | Buffer | Readable;
}
2 changes: 1 addition & 1 deletion packages/web/src/Transport/Response/TextPlainResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {ResponseAbstract} from "./ResponseAbstract.js";
export class TextPlainResponse extends ResponseAbstract<string> {
public readonly type: string = "text/plain";

public stringify(data: string): string {
public serialize(data: string): string {
return data;
}
}
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1886,10 +1886,10 @@ __metadata:
languageName: node
linkType: hard

"@types/node@npm:^20.4.2":
version: 20.4.2
resolution: "@types/node@npm:20.4.2"
checksum: 99e544ea7560d51f01f95627fc40394c24a13da8f041121a0da13e4ef0a2aa332932eaf9a5e8d0e30d1c07106e96a183be392cbba62e8cf0bf6a085d5c0f4149
"@types/node@npm:^20.4.4":
version: 20.4.4
resolution: "@types/node@npm:20.4.4"
checksum: 43f3c4a8acc38ae753e15a0e79bae0447d255b3742fa87f8e065d7b9d20ecb0e03d6c5b46c00d5d26f4552160381a00255f49205595a8ee48c2423e00263c930
languageName: node
linkType: hard

Expand Down Expand Up @@ -2694,7 +2694,7 @@ __metadata:
"@commitlint/cli": ^17.6.7
"@commitlint/config-conventional": ^17.6.7
"@types/jest": ^29.5.3
"@types/node": ^20.4.2
"@types/node": ^20.4.4
"@typescript-eslint/eslint-plugin": ^6.1.0
"@typescript-eslint/parser": ^6.1.0
cross-env: ^7.0.3
Expand Down

0 comments on commit 008e95c

Please sign in to comment.